OpenGL全景視頻

来源:http://www.cnblogs.com/betterwgo/archive/2017/05/31/6924345.html
-Advertisement-
Play Games

全景視頻其實在實現上和一般的視頻播放基本差不多,解碼可以用ffmpeg,只是對解碼後的圖片在繪製的時候要繪製在一個球上(我這裡是球,好像有說有的格式要繪製在四面體上的,美做深入研究),而不是畫在一個錶面上。所以這裡應該要用紋理。 1.計算球的頂點坐標和紋理坐標 球的頂點坐標和紋理坐標的計算可以說是全 ...


    全景視頻其實在實現上和一般的視頻播放基本差不多,解碼可以用ffmpeg,只是對解碼後的圖片在繪製的時候要繪製在一個球上(我這裡是球,好像有說有的格式要繪製在四面體上的,美做深入研究),而不是畫在一個錶面上。所以這裡應該要用紋理。

1.計算球的頂點坐標和紋理坐標

    球的頂點坐標和紋理坐標的計算可以說是全景的關鍵。這裡參考android opengl播放全景視頻 

int cap_H = 1;//必須大於0,且cap_H應等於cap_W
int cap_W = 1;//繪製球體時,每次增加的角度

float* verticals;
float* UV_TEX_VERTEX;

........................

void getPointMatrix(GLfloat radius)
{
    verticals = new float[(180 / cap_H) * (360 / cap_W) * 6 * 3];
    UV_TEX_VERTEX = new float[(180 / cap_H) * (360 / cap_W) * 6 * 2];

    float x = 0;
    float y = 0;
    float z = 0;

    int index = 0;
    int index1 = 0;
    float r = radius;//球體半徑
    double d = cap_H * PI / 180;//每次遞增的弧度
    for (int i = 0; i < 180; i += cap_H) {
        double d1 = i * PI / 180;
        for (int j = 0; j < 360; j += cap_W) {
            //獲得球體上切分的超小片矩形的頂點坐標(兩個三角形組成,所以有六點頂點)
            double d2 = j * PI / 180;
            verticals[index++] = (float)(x + r * sin(d1 + d) * cos(d2 + d));
            verticals[index++] = (float)(y + r * cos(d1 + d));
            verticals[index++] = (float)(z + r * sin(d1 + d) * sin(d2 + d));
            //獲得球體上切分的超小片三角形的紋理坐標
            UV_TEX_VERTEX[index1++] = (j + cap_W) * 1.0f / 360;
            UV_TEX_VERTEX[index1++] = (i + cap_H) * 1.0f / 180;

            verticals[index++] = (float)(x + r * sin(d1) * cos(d2));
            verticals[index++] = (float)(y + r * cos(d1));
            verticals[index++] = (float)(z + r * sin(d1) * sin(d2));

            UV_TEX_VERTEX[index1++] = j * 1.0f / 360;
            UV_TEX_VERTEX[index1++] = i * 1.0f / 180;

            verticals[index++] = (float)(x + r * sin(d1) * cos(d2 + d));
            verticals[index++] = (float)(y + r * cos(d1));
            verticals[index++] = (float)(z + r * sin(d1) * sin(d2 + d));

            UV_TEX_VERTEX[index1++] = (j + cap_W) * 1.0f / 360;
            UV_TEX_VERTEX[index1++] = i * 1.0f / 180;

            verticals[index++] = (float)(x + r * sin(d1 + d) * cos(d2 + d));
            verticals[index++] = (float)(y + r * cos(d1 + d));
            verticals[index++] = (float)(z + r * sin(d1 + d) * sin(d2 + d));

            UV_TEX_VERTEX[index1++] = (j + cap_W) * 1.0f / 360;
            UV_TEX_VERTEX[index1++] = (i + cap_H) * 1.0f / 180;

            verticals[index++] = (float)(x + r * sin(d1 + d) * cos(d2));
            verticals[index++] = (float)(y + r * cos(d1 + d));
            verticals[index++] = (float)(z + r * sin(d1 + d) * sin(d2));

            UV_TEX_VERTEX[index1++] = j * 1.0f / 360;
            UV_TEX_VERTEX[index1++] = (i + cap_H) * 1.0f / 180;

            verticals[index++] = (float)(x + r * sin(d1) * cos(d2));
            verticals[index++] = (float)(y + r * cos(d1));
            verticals[index++] = (float)(z + r * sin(d1) * sin(d2));

            UV_TEX_VERTEX[index1++] = j * 1.0f / 360;
            UV_TEX_VERTEX[index1++] = i * 1.0f / 180;
        }
    }
}

2.文件解碼

    我這裡用ffmpeg來做文件的解碼,用了一個最簡單的單線程迴圈來做,沒有做過多複雜的考慮。解出來的圖像數據放到一個迴圈隊列中。

DWORD WINAPI ThreadFunc(LPVOID n)
{
    AVFormatContext    *pFormatCtx;
    int                i, videoindex;
    AVCodec            *pCodec;
    AVCodecContext    *pCodecCtx = NULL;

    char filepath[] = "H:\\F-5飛行.mp4";

    av_register_all();
    avformat_network_init();
    pFormatCtx = avformat_alloc_context();

    if (avformat_open_input(&pFormatCtx, filepath, NULL, NULL) != 0){
        printf("Couldn't open input stream.(無法打開輸入流)\n");
        return -1;
    }

    if (avformat_find_stream_info(pFormatCtx, NULL)<0)
    {
        printf("Couldn't find stream information.(無法獲取流信息)\n");
        return -1;
    }

    videoindex = -1;
    for (i = 0; i<pFormatCtx->nb_streams; i++)
    if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
    {
        videoindex = i;
        break;
    }
    if (videoindex == -1)
    {
        printf("Didn't find a video stream.(沒有找到視頻流)\n");
        return -1;
    }
    pCodecCtx = pFormatCtx->streams[videoindex]->codec;
    pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    if (pCodec == NULL)
    {
        printf("Codec not found.(沒有找到解碼器)\n");
        return -1;
    }
    if (avcodec_open2(pCodecCtx, pCodec, NULL)<0)
    {
        printf("Could not open codec.(無法打開解碼器)\n");
        return -1;
    }

    AVFrame    *pFrame;
    pFrame = av_frame_alloc();
    int ret, got_picture;
    AVPacket *packet = (AVPacket *)av_malloc(sizeof(AVPacket));

    AVFrame *pFrameBGR = NULL;
    pFrameBGR = av_frame_alloc();

    struct SwsContext *img_convert_ctx;

    int index = 0;
    while (av_read_frame(pFormatCtx, packet) >= 0)
    {
        if (packet->stream_index == videoindex)
        {
            ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
            if (ret < 0)
            {
                printf("Decode Error.(解碼錯誤)\n");
                continue;
            }
            if (got_picture)
            {
                index++;

flag_wait:
                if (frame_queue.size >= MAXSIZE)
                {
                    printf("size = %d   I'm WAITING ... \n", frame_queue.size);
                    Sleep(10);
                    goto flag_wait;
                }

                EnterCriticalSection(&frame_queue.cs);

                Vid_Frame *vp;
                vp = &frame_queue.queue[frame_queue.rear];

                vp->frame->pts = pFrame->pts;

                /* alloc or resize hardware picture buffer */
                if (vp->buffer == NULL || vp->width != pFrame->width || vp->height != pFrame->height)
                {
                    if (vp->buffer != NULL)
                    {
                        av_free(vp->buffer);
                        vp->buffer = NULL;
                    }

                    int iSize = avpicture_get_size(AV_PIX_FMT_BGR24, pFrame->width, pFrame->height);
                    av_free(vp->buffer);
                    vp->buffer = (uint8_t *)av_mallocz(iSize);


                    vp->width = pFrame->width;
                    vp->height = pFrame->height;

                }

                avpicture_fill((AVPicture *)vp->frame, vp->buffer, AV_PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height);

                if (vp->buffer)
                {

                    img_convert_ctx = sws_getContext(vp->width, vp->height, (AVPixelFormat)pFrame->format, vp->width, vp->height,
                        AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);
                    sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize, 0, vp->height, vp->frame->data, vp->frame->linesize);
                    sws_freeContext(img_convert_ctx);

                    vp->pts = pFrame->pts;
                }
                    
                frame_queue.size++;
                frame_queue.rear = (frame_queue.rear + 1) % MAXSIZE;

                LeaveCriticalSection(&frame_queue.cs);

                //MySaveBmp("f5.bmp", vp->buffer, vp->width, vp->height);

                //int nHeight = vp->height;
                //int nWidth = vp->width;

                //Mat tmp_mat = Mat::zeros(nHeight, nWidth, CV_32FC3);

                //int k = 0;
                //for (int i = 0; i < nHeight; i++)
                //{
                //    for (int j = 0; j < nWidth; j++)
                //    {
                //        tmp_mat.at<Vec3f>(i, j)[0] = vp->buffer[k++] / 255.0f;
                //        tmp_mat.at<Vec3f>(i, j)[1] = vp->buffer[k++] / 255.0f;
                //        tmp_mat.at<Vec3f>(i, j)[2] = vp->buffer[k++] / 255.0f;
                //    }
                //}

                //imwrite("mat_Image.jpg", tmp_mat);

                //namedWindow("Marc_Antony");
                //imshow("Marc_Antony", tmp_mat);

                //waitKey(0);
                
            }
        }
        av_free_packet(packet);
    }

    avcodec_close(pCodecCtx);
    avformat_close_input(&pFormatCtx);

    return 0;
}

其中frame_queue是一個迴圈隊列,解碼的時候入隊,渲染的時候出隊。雖然沒有實際測,但我試用的幾個視頻文件都是4K的,所以解碼時間估計有些長,解碼這裡如果能用硬解應該效果會更好。然後我這裡沒有考慮音頻。

3.渲染

(1)初始化

void init(void)
{
    initQueue(&frame_queue);

    glGenTextures(1, &texturesArr);    //創建紋理

    glBindTexture(GL_TEXTURE_2D, texturesArr);

    //Mat image = imread("1.jpg");
    //glTexImage2D(GL_TEXTURE_2D, 0, 3, image.cols, image.rows, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, image.data);

    //IplImage *image = cvLoadImage("4.png", 1);
    //IplImage *image = cvLoadImage("5.png", 1);
    //glTexImage2D(GL_TEXTURE_2D, 0, 3, image->width, image->height, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, image->imageData);
    //printf("nChannels is %d \n", image->nChannels);
    //cvNamedWindow("1");
    //cvShowImage("1", image);
    //cvWaitKey(0);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);    //線形濾波
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);    //線形濾波


    glClearColor(0.0, 0.0, 0.0, 0.0);
    glClearDepth(1);
    glShadeModel(GL_SMOOTH);
    //GLfloat _ambient[] = { 1.0, 1.0, 1.0, 1.0 };
    //GLfloat _diffuse[] = { 1.0, 1.0, 1.0, 1.0 };
    //GLfloat _specular[] = { 1.0, 1.0, 1.0, 1.0 };
    //GLfloat _position[] = { 255, 255, 255, 0 };
    //glLightfv(GL_LIGHT0, GL_AMBIENT, _ambient);
    //glLightfv(GL_LIGHT0, GL_DIFFUSE, _diffuse);
    //glLightfv(GL_LIGHT0, GL_SPECULAR, _specular);
    //glLightfv(GL_LIGHT0, GL_POSITION, _position);
    //glEnable(GL_LIGHTING);
    //glEnable(GL_LIGHT0);

    glEnable(GL_TEXTURE_2D);
    glEnable(GL_DEPTH_TEST);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

    glDisable(GL_CULL_FACE);    //禁用裁剪

    getPointMatrix(500);
}

初始化中包含隊列的初始化,創建紋理,計算球的頂點坐標和紋理坐標,各種參數設置。其中註意急用光源,否則圖片各處的明暗會依據光源位置的設置而有不同;其次是禁用剪裁,否則無法進入到球體內部,因為全景視頻是在球體內部看的。

(2)設置投影矩陣

void reshape(int w, int h)
{
    glViewport(0, 0, (GLsizei)w, (GLsizei)h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    //glOrtho(-250.0, 250, -250.0, 250, -500, 500);
    //glFrustum(-250.0, 250, -250.0, 250, -5, -500);
    gluPerspective(45, (GLfloat)w / h, 1.0f, 1000.0f);    //設置投影矩陣
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

投影採用透視投影,這樣可以進進球體內部。這裡角度設置成45,可以自行設置,但不宜過大,過大效果不是很好。

(3)渲染

void display(void)
{
    glLoadIdentity();
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    gluLookAt(0, 0, distance, 0, 0, 500.0, 0, 1, 0);
    printf("distance: %f \n", distance);
    glRotatef(xangle, 1.0f, 0.0f, 0.0f);    //繞X軸旋轉
    glRotatef(yangle, 0.0f, 1.0f, 0.0f);    //繞Y軸旋轉
    glRotatef(zangle, 0.0f, 0.0f, 1.0f);    //繞Z軸旋轉

    EnterCriticalSection(&frame_queue.cs);

    printf("display size = %d \n", frame_queue.size);
    if (frame_queue.size > 0)
    {
        Vid_Frame *vp = &frame_queue.queue[frame_queue.front];

        glBindTexture(GL_TEXTURE_2D, texturesArr);
        glTexImage2D(GL_TEXTURE_2D, 0, 3, vp->width, vp->height, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, vp->buffer);

        frame_queue.size--;
        frame_queue.front = (frame_queue.front + 1) % MAXSIZE;
    }

    LeaveCriticalSection(&frame_queue.cs);

    //glColor3f(1.0, 0.0, 0.0);
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glVertexPointer(3, GL_FLOAT, 0, verticals);
    glTexCoordPointer(2, GL_FLOAT, 0, UV_TEX_VERTEX);
    glPushMatrix();
    glDrawArrays(GL_TRIANGLES, 0, (180 / cap_H) * (360 / cap_W) * 6);
    glPopMatrix();
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisableClientState(GL_VERTEX_ARRAY);  // disable vertex arrays

    glFlush();

    av_usleep(25000);
}

渲染時把解出來的數據從隊列中取出生成新的紋理。渲染採用glDrawArrays函數,使用的GL_TRIANGLES參數,使用這個參數對於計算球的頂點坐標和紋理坐標來說不需要考慮很多,比較方便,就是點數過多的時候可能會影響渲染的效率。

(5)畫面更新與重繪

void reDraw(int millisec)
{
    glutTimerFunc(millisec, reDraw, millisec);
    glutPostRedisplay();
}

這裡用OpenGL的定時器來對畫面做一個定時的更新,從而實現視頻播放的效果。

4.一些控制操作

(1)鍵盤控制

void keyboard(unsigned char key, int x, int y)
{
    switch (key)
    {
    case 'x':        //當按下鍵盤上d時,以沿X軸旋轉為主
        xangle += 1.0f;    //設置旋轉增量
        break;
    case 'X':
        xangle -= 1.0f;    //設置旋轉增量
        break;
    case 'y':
        yangle += 1.0f;
        break;
    case 'Y':
        yangle -= 1.0f;
        break;
    case 'z':
        zangle += 1.0f;
        break;
    case 'Z':
        zangle -= 1.0f;
        break;
    case 'a':
        distance += 10.0f;
        break;
    case 'A':
        distance -= 10.0f;
        break;
    default:
        return;
    }
    glutPostRedisplay();    //重繪函數
}

用鍵盤來實現球體繞x,y,z軸的旋轉,以及觀察球體的距離。

(2)滑鼠控制

//處理滑鼠點擊
void Mouse(int button, int state, int x, int y)
{
    if (state == GLUT_DOWN) //第一次滑鼠按下時,記錄滑鼠在視窗中的初始坐標
    {
        //記住滑鼠點擊後游標坐標
        cx = x;
        cy = y;
    }
}

//處理滑鼠拖動
void onMouseMove(int x, int y)
{
    float offset = 0.18;
    //計算拖動後的偏移量,然後進行xy疊加減
    yangle -= ((x - cx) * offset);

    if ( y > cy) {//往下拉
        xangle += ((y - cy) * offset);
    }
    else if ( y < cy) {//往上拉
        xangle += ((y - cy) * offset);
    }

    glutPostRedisplay();

    //保存好當前拖放後游標坐標點
    cx = x;
    cy = y;
}

5.主函數

int main(int argc, char* argv[])
{
    glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB | GLUT_DEPTH);
    glutInitWindowSize(1640, 840);
    glutInitWindowPosition(100, 100);
    glutCreateWindow("OpenGL全景");
    init();
    glutReshapeFunc(reshape);
    glutDisplayFunc(display);
    glutKeyboardFunc(keyboard);
    glutMouseFunc(Mouse);
    glutMotionFunc(onMouseMove);

    glutTimerFunc(25, reDraw, 25);

    HANDLE hThrd = NULL;
    DWORD threadId;
    hThrd = CreateThread(NULL, 0, ThreadFunc, 0, 0, &threadId);

    glutMainLoop();

    WaitForSingleObject(hThrd, INFINITE);

    if (hThrd)
    {
        CloseHandle(hThrd);
    }

    return 0;
}

glutMainLoop()函數真是個噁心的函數,都沒找到正常退出他的方法,要退出貌似必須得把整個程式都退出去,在實際使用的時候大多數時候我們都只是希望退出迴圈就夠了,不一定要退出整個程式。所以如果用win32來做,最好就不要用這個函數,用一個獨立的線程來做渲染,各種消息通過win32來實現,這樣是比較方便的。

運行截圖:

image

 

 

工程源碼:http://download.csdn.net/download/qq_33892166/9856939

VR視頻推薦:http://dl.pconline.com.cn/vr/list0_1_2007_2018.html


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

-Advertisement-
Play Games
更多相關文章
  • 面向對象高級語法部分 通過@staticmethod裝飾器即可把其裝飾的方法變為一個靜態方法,什麼是靜態方法呢?其實不難理解,普通的方法,可以在實例化後直接調用,並且在方法里可以通過self.調用實例變數或類變數,但靜態方法是不可以訪問實例變數或類變數的,一個不能訪問實例變數和類變數的方法,其實相當 ...
  • 一、先新建 empty project, 再新建 module 首先,新建一個 empty project 填寫 project 名字及存放位置 project 新建完後會打開 idea 編輯器視窗,選擇 File/New/Module 新建 module 左側選擇 Java Enterprise, ...
  • 分析網站 首先來到目標數據的網頁 http://www.weather.com.cn/weather40d/101280701.shtml 中國天氣網 中國天氣網 我們可以看到,我們需要的天氣數據都是放在圖表上的,在切換月份的時候,發現只有部分頁面刷新了,就是天氣數據的那塊,而URL沒有變化。 這是 ...
  • 數據校驗在web應用里是非常重要的功能,尤其是在表單輸入中。在這裡採用Hibernate-Validator進行校驗,該方法實現了JSR-303驗證框架支持註解風格的驗證。 一、導入jar包 若要實現數據校驗功能,需要導入必要的jar包,主要包括以下幾個: classmate-1.3.1.jar h ...
  • 結果 結果 對過程沒有興趣的童鞋直接看這裡啦。 評論數大於五萬的歌曲排行榜 首先恭喜一下我最喜歡的歌手(之一)周傑倫的《晴天》成為網易雲音樂第一首評論數過百萬的歌曲! 通過結果發現目前評論數過十萬的歌曲正好十首,通過這前十首發現: 薛之謙現在真的很火啦~ 幾乎都是男歌手啊,男歌手貌似更受歡迎?(別打 ...
  • 1、java.lang.ArithmeticException 算術運算異常,例如除數為0,所以引發了算數異常 2、Java.lang.StringIndexOutOfBoundsException: 這是截取字元串substring()產生的下標越界異常。原因是可能是字元串為空,或長度不足1 3、 ...
  • 這一篇中,我們繼續繼續進行我們的坦克大戰。 位置信息數據結構 在游戲設計過程中,需要記錄大量的位置信息,如果僅僅使用(x,y)坐標很容易出錯。這一篇中,我們先定義兩個簡單的數據結構用來保存點和矩形的信息。 在項目中新建Model目錄,創建下麵四個文件: 代碼如下: Point.h 這個頭文件創建了一 ...
  • java對象中primitive類型變數可以通過不提供set方法保證不被修改,但對象的List成員在提供get方法後,就可以隨意add、remove改變其結構,這不是希望的結果。網上看了下,發現Collections的靜態方法unmodifiableList可以達到目的。方法原型為:public s ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...