QQ視頻通話、抖音的視頻回顯 是如何實現的

来源:https://www.cnblogs.com/xiaxveliang/archive/2020/03/02/12395861.html
-Advertisement-
Play Games

QQ視頻通話、抖音的視頻回顯 是如何實現的 先說為什麼會有這一篇文章: 2014年聯想曾經做過一款 短視頻軟體,叫“魔力秀”。可以說和現在的抖音基本是一樣的,但因為“魔力秀App”出生於聯想,註定無法在一個硬體公司成長為一棵參天大樹,最終只發了一個版本就結束了。 當時“魔力秀App”的視頻回顯模塊是 ...


QQ視頻通話、抖音的視頻回顯 是如何實現的

先說為什麼會有這一篇文章:
2014年聯想曾經做過一款 短視頻軟體,叫“魔力秀”。可以說和現在的抖音基本是一樣的,但因為“魔力秀App”出生於聯想,註定無法在一個硬體公司成長為一棵參天大樹,最終只發了一個版本就結束了。
當時“魔力秀App”的視頻回顯模塊是我設計實現的,所以就有了這篇文章。
事過多年,將這篇文章拿出來整理,因為這項技術依然不過時,反而被廣泛應用...

這篇文章之前叫做 Opengl ES中YUV420轉RGB 是一個技術標題。整理時,發現用這個標題,大家實際是不知道這個技術有什麼用,因此換了這個比較醒目的名字。

Opengl ES中YUV420轉RGB 這項技術主要是實現視頻高效、節省帶寬的回顯視頻圖像。

  • 為什麼說高效?
    因為直接用 OpenGL ES 實現,本身繞開了Androi的層層封裝;
    而且Opengl 本身就是圖形學介面,實現效率天然高效。
  • 為什麼說節省帶寬?
    因為網路傳輸中,採用的YUV420數據格式,本身是一種有損的數據格式。但由於格式的特性,色彩還原後基本對圖像顯示效果沒有影響,因此在視頻通話場景中廣泛使用。

這裡通過以下幾個方面具體說明Opengl ES中YUV420轉RGB 這項技術的實現方式:

  • 先瞭解一個概念“灰度圖”
  • YUV數據格式
  • YUV444和YUV420
  • YUV420轉RGB
  • OpenGL ES中YUV420P轉RGB

一、先瞭解一個概念“灰度圖”

這裡先瞭解一下灰度 Y 的概念。不知道大家是否看過老式的黑白電視機
老式黑白電視機的圖像就只有Y一個通道,老式黑白電視機上的圖像就是灰度圖成像(只用接收一個Y通道數據就能播放出電視畫面,前輩們果然厲害... ;而後來的彩色電視用的是YUV數據信號,這樣既相容了老的黑白電視,又可以在新式彩色電視上顯示彩色圖像,前輩們太厲害了...)

  • 灰度圖的定義:
  • 灰度值與RGB的計算公式
  • 將“彩色圖轉”轉化為“灰度圖”shader實現

1.1、灰度圖的定義:

把白色與黑色之間按對數關係分為若幹等級,稱為灰度。灰度分為256階。

1.2、灰度值Y與RGB的計算公式:

Y = 0.299R + 0.587G + 0.114*B

電視臺發出信號時,將RGB數據這樣轉化為Y 數據。老式黑白電視機接收到Y信號,就能展示圖象了。

1.4、將“彩色圖轉”轉化為“灰度圖”shader實現

這裡說一個技術實現,在OpenGL ES中,如何用shader片元著色器,把一個彩色紋理圖轉化為一個灰度圖?

效果如下:

彩色圖

轉化後的灰度圖

轉化當然要用到我們上邊說道的RGB 轉 Y的公式,下邊我們看具體的片源著色器 shader 代碼實現:

// shader 片元著色器
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D sTexture;

void main() {
        // 從紋理圖sTexture 讀取當前片元的RGB顏色
         vec4 color=texture2D(sTexture, vTextureCoord);
         // 公式計算灰度值
         float col=color.r*0.299+color.g*0.587+color.b*0.114;
         // 將生成的Y 灰度值設置給RGB通道
         color.r=col;
         color.g=col;
         color.b=col;
         // 傳給片源著色器
         gl_FragColor =color;
}

在shader實現中,我特意加了註釋。
瞭解glsl語法的同學,可以仔細讀一下上邊的代碼實現;
當然不瞭解語法的同學,更要簡單讀一遍(glsl是一種類C語言,只要學過C語言應該就能讀懂)

二、YUV數據格式

上邊我們瞭解了灰度圖的實現,這裡我們介紹一個YUV數據格式。
主要分為以下幾個部分:

  • YUV定義
  • 使用YUV的好處
  • YUV與RGB轉換公式
  • YUV444和YUV420

2.1、YUV

YUV的具體定義如下:

Y:就是灰度值;
UV:用來指定像素的顏色。

對於UV現在有些懵沒關係,我們繼續往下看

2.2、YUV與RGB轉換公式

// RGB轉YUV
Y= 0.299*R + 0.587*G + 0.114*B
U= -0.147*R - 0.289*G + 0.436*B = 0.492*(B- Y)
V= 0.615*R - 0.515*G - 0.100*B = 0.877*(R- Y)
//############################################
// YUV轉RGB
R = Y + 1.140*V
G = Y - 0.394*U - 0.581*V
B = Y + 2.032*U

2.3、使用YUV的好處:

  • 傳輸信號向後相容老式黑白電視機(用於優化彩色視頻信號的傳輸,使其向後相容老式黑白電視)
  • YUV420占用的帶寬少(這個我們在前邊提過,至於具體為什麼,後邊會有詳細介紹)

YUV420與RGB視頻信號傳輸相比,它最大的優點在於只需占用極少的頻寬(後面來介紹)

2.4、YUV444和YUV420

前邊我們一直說的YUV數據,其實是YUV420數據格式。YUV420數據格式在傳輸上UV色彩是有損傳輸,而YUV444 其實是一種無損的數據格式。
那為什麼這裡我們還是要說一下YUV444格式呢?
其實是為了後邊實現 將YUV420數據還原成RGB做準備

首先介紹YUV444 數據格式:

  • YUV444:
    一個像素點對應一個Y一個U一個V(YUV一一對應)
    YUV444數據格式 如下圖所示:

YUV444數據格式

YUV444 中YUV通道一一對應,理解簡單。下邊這個是YUV420數據格式,UV數據有損。

  • YUV420:
    一個像素點對應一個Y;
    四個像素點對應一個U一個V;

具體數據格式如下:

YUV420數據格式

從上圖可以看到,UV色彩通道是有損失的,這也是為什麼YUV420在展示時,占用的帶寬更少一下。

a、Y、U、V沒有一一對應,圖像有顏色損失
b、這也就是為什麼占用的帶寬少了;
c、同樣網路傳輸中,占用的流量也同樣減少了;
d、但對圖像的色彩展示幾乎沒有影響

因為占用的流量較少,對色彩展示幾乎沒有影響,因此廣泛應用於各中視頻通話場景,視頻回顯場景等。

## 三、YUV420轉RGB

哇去,基礎知識終於說完了,這裡說到我們的核心技術點:YUV420轉RGB

  • 第一個步驟YUV420轉YUV444;
  • 第二個步驟YUV444轉RGB。

為什麼要把YUV420轉為YUV444?
因為在傳輸時,YUV420中的UV通道數據損失了。但我們渲染時,需要把這損失掉的UV色彩數據通道還原回來,再進行YUV444 轉 RGB

先說 YUV420 轉 YUV444

3.1、YUV420轉YUV444

要把YUV420轉為YUV444就得把“上圖 YUV420” U與V中 “?” 的部分填滿。

通過YUV420數據中,已有的U 與 Y數據,通過差值計算的方式,填補上空缺的部分。以下是差值運算的具體實現公式,差值計算如下(建議參照YUV420數據格式圖來看,要不容易懵):

U01 = (U00 + U02)/2; // 利用已有的 U00、U02來計算U01
U10 = (U00 + U20)/2; // 利用已有的 U00、U20來計算U10
U11 = (U00 + U02 + U20 + U22)/4;// 利用已有的 U00、U02、U20、U22來計算U11

//######################
V01 = (V00 + V02)/2; // 利用已有的 V00、V02來計算V01
V10 = (V00 + V20)/2; // 利用已有的 V00、V20來計算V10
V11 = (V00 + V02 + V20 + V22)/4; // 利用已有的 V00、V02、V20、V22來計算V11

經過以上公式,YUV420轉YUV444 完成(數據補全成功),下邊來說YUV444如何轉RGB。

3.2、YUV444轉RGB

YUV444轉RGB是有現成公式的,我們直接拿來用就行了,YUV轉RGB的公式:

R = Y + 1.140*V
G = Y - 0.394*U - 0.581*V
B = Y + 2.032*U

公式有了,那具體的代碼實現是怎麼實現的呢?

註:
一、二、三、四,這四點介紹的是YUV轉RGB的基本原理,下邊是具體實現。

四、OpenGL ES中YUV420P轉RGB

這一節介紹具體技術實現,但開始時,還是要介紹兩種數據格式(哎、我知道你們都煩了,我其實也煩,但還是得說)

  • YUV420p的數據格式
  • YUV420sp的數據格式(YUV420sp轉RGB這裡不做介紹)
  • YUV420sp 轉RGB

4.1、YUV420p的數據格式

YUV420p的數據格式如下圖所示(為一個byte[]):

YUV420p

其中數據的4/6為Y;1/6為U;1/6為V。

4.2、YUV420sp的數據格式(YUV420sp轉RGB這裡不做介紹)

YUV420sp的數據格式如下圖所示(為一個byte[]):

YUV420sp

其中數據的4/6為Y;1/6為U;1/6為V。

4.3、YUV420sp 轉RGB

其實大概原理就是:

  • 將YUV420數據中的Y U V 數據分別取出來,分別生成三張紋理圖
  • 利用片元著色器每個片元執行一次的特性,將YUV420數據轉為YUV444數據
  • 從YUV444數據中,取出一一對應的YUV數據
  • 最後,利用公式YUV444 轉 RGB
  • 完事大吉

以下為YUV三張紋理圖效果圖:

YUV三張紋理圖

YUV420轉YUV444

這裡如何補全YUV420數據中UV部分的顏色數據?

這裡有一個討巧的方式:
在OpenGL ES生成紋理時,採用線性紋理採樣方式。線性採樣出U、V紋理中“?”部分的顏色值。這樣就就可以拿到一一對應的YUV444數據。

對應的Java代碼如下:

    /**
     * 
     * @param w
     * @param h
     * @param date
     *            數據
     * @param textureY
     * @param textureU
     * @param textureV
     * @param isUpdate
     *            是否為更新
     */
    public static boolean bindYUV420pTexture(int frameWidth, int frameHeight,
            byte frameData[], int textureY, int textureU, int textureV,
            boolean isUpdate) {

        if (frameData == null || frameData.length == 0) {
            return false;
        }
        Log.d(TAG, "----bindYUV420pTexture-----");

        if (isUpdate == false) {

            /**
             * 數據緩衝區
             */
            // Y
            ByteBuffer buffer = LeBuffer.byteToBuffer(frameData);
            // GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureY);

            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);// GL_LINEAR_MIPMAP_NEAREST
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

            /**
             * target 指定目標紋理,這個值必須是GL_TEXTURE_2D; level
             * 執行細節級別,0是最基本的圖像級別,n表示第N級貼圖細化級別; internalformat
             * 指定紋理中的顏色組件,可選的值有GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE,
             * GL_LUMINANCE_ALPHA 等幾種; width 指定紋理圖像的寬度; height 指定紋理圖像的高度; border
             * 指定邊框的寬度; format 像素數據的顏色格式,可選的值參考internalformat; type
             * 指定像素數據的數據類型,可以使用的值有GL_UNSIGNED_BYTE
             * ,GL_UNSIGNED_SHORT_5_6_5,GL_UNSIGNED_SHORT_4_4_4_4
             * ,GL_UNSIGNED_SHORT_5_5_5_1; pixels 指定記憶體中指向圖像數據的指針;
             * 
             */
            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
                    frameWidth, frameHeight, 0, GLES20.GL_LUMINANCE,
                    GLES20.GL_UNSIGNED_BYTE, buffer);

            /**
             * 
             */
            // U
            buffer.clear();
            buffer = LeBuffer.byteToBuffer(frameData);
            buffer.position(frameWidth * frameHeight);
            //
            // GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureU);

            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);// GL_LINEAR_MIPMAP_NEAREST
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
                    frameWidth / 2, frameHeight / 2, 0, GLES20.GL_LUMINANCE,
                    GLES20.GL_UNSIGNED_BYTE, buffer);

            /**
             * 
             */
            // V
            buffer.clear();
            buffer = LeBuffer.byteToBuffer(frameData);
            buffer.position(frameWidth * frameHeight * 5 / 4);
            //
            // GLES20.glActiveTexture(GLES20.GL_TEXTURE2);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureV);

            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);// GL_LINEAR_MIPMAP_NEAREST
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
                    frameWidth / 2, frameHeight / 2, 0, GLES20.GL_LUMINANCE,
                    GLES20.GL_UNSIGNED_BYTE, buffer);

        } else {
            /**
             * Y
             */
            ByteBuffer buffer = LeBuffer.byteToBuffer(frameData);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureY);
            GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, frameWidth,
                    frameHeight, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE,
                    buffer);

            /**
             * U
             */
            //
            buffer.clear();
            buffer = LeBuffer.byteToBuffer(frameData);
            buffer.position(frameWidth * frameHeight);
            //
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureU);
            GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0,
                    frameWidth / 2, frameHeight / 2, GLES20.GL_LUMINANCE,
                    GLES20.GL_UNSIGNED_BYTE, buffer);
            /**
             * V
             */
            //
            buffer.clear();
            buffer = LeBuffer.byteToBuffer(frameData);
            buffer.position(frameWidth * frameHeight * 5 / 4);
            //
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureV);
            GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0,
                    frameWidth / 2, frameHeight / 2, GLES20.GL_LUMINANCE,
                    GLES20.GL_UNSIGNED_BYTE, buffer);
        }
        return true;
    }

代碼說明:
已上代碼便是將傳入的幀數據byte frameData[],轉為三張紋理圖的代碼。
代碼的16行、5051行、7576行分別為從byte frameData[]中分別取出Y、U、V數據的代碼。
代碼5659行、代碼8184行分別為設置U、V紋理的採樣方式為線性採樣的代碼。
以上代碼運行結束,記憶體中會生成三張紋理圖像。
將三張紋理圖像傳入“片元著色器”執行下一步驟。

YUV444轉RGB

YUV一一對應的紋理有了,這裡該介紹如何實現YUV444轉RGB了:

按照YUV轉RGB的公式,將Y、U、V一一對應的取出,進行YUV轉RGB操作,生成像素點。

對應片元著色器 shader 代碼實現:

recision mediump float;
// 片元著色器中 輸入了Y U V三張紋理
uniform sampler2D sTexture_y;
uniform sampler2D sTexture_u;
uniform sampler2D sTexture_v;

varying vec2 vTextureCoord;

//YUV 轉 RGB的 shader 實現
void getRgbByYuv(in float y, in float u, in float v, inout float r, inout float g, inout float b){  
    //
    y = 1.164*(y - 0.0625);
    u = u - 0.5;
    v = v - 0.5;
    //
    r = y + 1.596023559570*v;
    g = y - 0.3917694091796875*u - 0.8129730224609375*v;
    b = y + 2.017227172851563*u;
}

void main() {
    //
    float r,g,b;
    
    // 從YUV三張紋理中,採樣出一一對應的YUV數據
    float y = texture2D(sTexture_y, vTextureCoord).r;
    float u = texture2D(sTexture_u, vTextureCoord).r;
    float v = texture2D(sTexture_v, vTextureCoord).r;
    // YUV 轉 RGB
    getRgbByYuv(y, u, v, r, g, b);
    
    // 最終顏色賦值
    gl_FragColor = vec4(r,g,b, 1.0); 
}

五、完事大吉

源碼真的是懶得整理,所以,大家還是理解了實現原理,自己動手去敲吧,不要找我要代碼了!!!
源碼真的是懶得整理,所以,大家還是理解了實現原理,自己動手去敲吧,不要找我要代碼了!!!
源碼真的是懶得整理,所以,大家還是理解了實現原理,自己動手去敲吧,不要找我要代碼了!!!

========== THE END ==========

wx_gzh.jpg


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 我們在用MAT(Memory Analyzer Tool)分析Android記憶體時,會發現大量的bitmap對象占了記憶體使用。但是很難定位究竟是哪張圖片占用了記憶體,這裡介紹一種查看bitmap的方法。 MAT、GIMP下載 MAT http://www.eclipse.org/mat/downloa ...
  • SparseArray源碼來自:android 25/java/util/SparseArray ArrayMap源碼來自:25.3.1/support compat 25.3.1/android/android.support.v4.util.ArrayMap 一、SparseArray實現源碼學 ...
  • 英文原文地址 "Memory optimization for feeds on Android" 讀後感 在Java中HashSet只能存放繼承自Objcet的對象,這中情況下“基本數據類型”轉化為繼承自Object的( 、`Long`等)會產生很多中間Object對象,占用過多的記憶體,從而引發垃 ...
  • 效果圖 實現源碼(已上傳我的GitHub): "https://github.com/xiaxveliang/GL_AUDIO_VIDEO_RECODE" 參考: "http://bigflake.com/mediacodec/EncodeAndMuxTest.java.txt" 對於以上代碼,我做 ...
  • obj文件是3D模型文件格式。由Alias|Wavefront公司為3D建模和動畫軟體"Advanced Visualizer"開發的一種標準,適合用於3D軟體模型之間的互導,也可以通過Maya讀寫。 + 只支持模型三角面數據和材質信息,無動畫功能支持; + 其中幾何信息由.obj文件提供,材質信息 ...
  • Mac下Jenkins Android打包 一、安裝tomcat a、下載tomcat http://tomcat.apache.org/ 下載完成後解壓到: b、啟動tomcat: c、驗證 二、安裝Jenkins a、下載 jenkins.war https://jenkins.io/index ...
  • 偶然發現 ,僅僅一個Java文件,可在嵌入式設備(例:Android手機)中啟動一個本地伺服器,接收客戶端本地部分請求。 認真學習了其源碼實現,這裡按照我的學習順序寫了一篇簡單的文章(算是學習筆記吧): + 瞭解官方描述 + 寫個Demo使用一下(Android中本地代理,播放Sdcard中的m3u ...
  • Android Q 深色主題舉例 瞭解深色主題如何應用,第一手資料是 "官方文檔" 與 相應的 "Google Sample" 官方文檔:DayNight — Adding a dark theme to your app: "https://medium.com/androiddevelopers ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...