1. 程式人生 > >區域網下,實現一鍵共享螢幕到移動裝置

區域網下,實現一鍵共享螢幕到移動裝置

區域網下,實現一鍵共享螢幕到移動裝置

1. 問題起因


開發需求

剛不久開發一款了教育類app,需要實現教師端對學生移動裝置進行遠端操控,比如對學生平板進行解鎖屏,共享電腦螢幕到學生端,監控學生螢幕內容等。

網路環境

教師端網線或WIFI接入,iPad和Android Pad通過WIFI接入,確保在一個網段下。

大致功能

graph TB S(Service<br/>教師端) S--一鍵解鎖/鎖定螢幕-->C1 S--一鍵分發檔案<br/>ppt/doc/img-->C2 S--螢幕廣播-->C3 S--學生搶答-->C4 S--實時監控-->C5 C1(Client1 <br/>iPad/Android Pad) C2(Client2 <br/>iPad/Android Pad) C3(Client3 <br/>iPad/Android Pad) C4(Client4 <br/>iPad/Android Pad) C5(Client4 <br/>iPad/Android Pad)

2. 實現方案


教師端採用FFmpeg採集螢幕音視訊,iOS、Android端使用ijkplayer拉流播放,流傳輸協議採用RTMP協議,通訊方式採用TCP Socket。

通訊實現

區域網內教師端充當伺服器傳送UDP廣播(內容包含本機IP和埠號),iOS、Android端收到UDP廣播獲取到IP地址和埠後,採用Socket【CocoaAsyncSocket(iOS)、Socket(Android)】與教師端進行TCP連線,建立連線完成後,通過Socket收發訊息進行通訊。

3. 技術模組

3.1 Mac下nginx-full搭建


1.Homebrew安裝

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

2.安裝nginx-full(rtmp)

brew install nginx-full --with-rtmp-module

3.檢視nginx安裝的路徑等資訊

brew info nginx-full

會顯示出配置檔案所在路徑

The default port has been set in /usr/local/etc/nginx/nginx.conf to 8080 so that
nginx can run without sudo.

4.配置nginx.conf,檔案最後空白處直接新增(application live,live隨便起名,之後推流對應就可以了)。

rtmp {
    server {
        listen 1935;
        application live {
            live on;
            record off;
        }
    }
}

5.修改儲存後重啟nginx

nginx -s reload

3.2 安裝ffmepg推流


1.安裝

brew install ffmpeg

2.推送螢幕流

ffmpeg -f avfoundation -pixel_format uyvy422 -i "1" -f flv rtmp://localhost:1935/live

執行後顯示Output地址,rtmp://localhost:1935/live,也就是本機ip,比如rtmp://192.168.1.2:1935/live,Mac電腦可以安裝VLC播放器,測試播放。

Output #0, flv, to 'rtmp://localhost:1935/live':
  Metadata:
    encoder         : Lavf58.20.100
    Stream #0:0: Video: flv1 (flv) ([2][0][0][0] / 0x0002), yuv420p, 2560x1600, q=2-31, 200 kb/s, 1000k fps, 1k tbn, 1000k tbc
    Metadata:
      encoder         : Lavc58.35.100 flv
    Side data:
      cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: -1
frame=  241 fps= 27 q=24.8 size=    5368kB time=00:00:08.86 bitrate=4958.5kbits/s speed=   1x     

3.3 IJKPlayer編譯


附件:iOS編譯後動態庫和Android庫檔案

參考ijkplayer文件說明,在mac下編譯即可,不過在編譯之前,需要修改一些配置檔案。如果想到達首屏秒開,務必看完這些內容再去編譯,包括後面講到的客戶端首屏秒開,因為涉及C檔案修改,省去之後又要重新編譯。

編譯之前一定仔細閱讀README.md,比如在編譯環境和所需檔案:

# install homebrew, git, yasm
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install git
brew install yasm

# add these lines to your ~/.bash_profile or ~/.profile
# export ANDROID_SDK=<your sdk path>
# export ANDROID_NDK=<your ndk path>

# on Cygwin (unmaintained)
# install git, make, yasm

還有就是他當時的編譯環境My Build Environment,這塊需要說明一下,尤其是編譯安卓的,NDK就直接用r10e,雖然之後的也可以,但是會有編譯失敗的可能,因為我編譯的時候就失敗了,更換為作者使用的版本通過。

Common
Mac OS X 10.11.5
Android
NDK r10e
Android Studio 2.1.3
Gradle 2.14.1
iOS
Xcode 7.3 (7D175)
HomeBrew
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install git

README.md 對應有Build iOS和Build Android,編譯哪個平臺就執行對應的命令。其中預設連結的指令碼是 less codec/format for smaller binary size,具體說明看文件,這裡我選擇的預設配置。

Build iOS編譯中,./init-ios.sh命令久一點,中間要下載一些東西,具體內容可以檢視指令碼檔案。比如== pull ffmpeg base ==,明顯要好久,除非你當時下載的速度很快。

== pull ffmpeg base ==
Cloning into 'extra/ffmpeg'...
remote: Enumerating objects: 538907, done.
Receiving objects:  19% (103984/538907), 30.82 MiB | 42.00 KiB/s   

3.4 iOS Framwork合併


一切順利完成後執行demo,編譯獲取動態庫,這邊我直接使用真機和模擬器合併的動態庫,當然你也可以不要合併,直接使用真機動態庫。

1.配置Release模式,Edit Scheme —> Run —> info —> Build Configuration —> Release 2.真機和模擬器各自編譯 3.Products —> IJKMediaFramework.framework —> Show in Finder 4.終端 cd Products 目錄下,執行:lipo -create 真機 模擬器 -output 合併檔案

lipo -create Release-iphoneos/IJKMediaFramework.framework/IJKMediaFramework Release-iphonesimulator/IJKMediaFramework.framework/IJKMediaFramework -output IJKMediaFramework

5.合併後的檔案替換掉真機framework下的檔案,新的IJKMediaFramework.framework就是合併後的動態庫,直接拖拽到專案使用。

iOS 編譯可能會遇到的問題和解決辦法

問題1:

./libavutil/arm/asm.S:50:9: error: unknown directive
        .arch armv7-a
        ^
make: *** [libavcodec/arm/aacpsdsp_neon.o] Error 1
make: *** Waiting for unfinished jobs....

解決辦法:

修改./compile-ffmpeg.sh檔案

將這一行:FF_ALL_ARCHS_IOS8_SDK="armv7 arm64 i386 x86_64"

修改為:FF_ALL_ARCHS_IOS8_SDK="arm64 i386 x86_64"

問題2:

'openssl/ssl.h' file not found
#include <openssl/ssl.h> ERROR: openssl not found

解決辦法:

編譯ffmpeg軟解碼庫,這個過程會生成各種架構的ffmpeg,編譯ffmpeg前要先compile OpenSSL,對openssl進行編譯,如果未執行可能會報錯。必須先執行./compile-openssl.sh all

實際編譯的確會遇到這些問題,尤其是問題1。

我的Xcode版本 Version 10.3 (10G8)

附件:iOS編譯後動態庫和Android庫檔案

問題這些參考了部落格iOS IJKPlayer 專案整合

3.5 客戶端首屏秒開


首屏秒開,需要結合視訊清晰度和延遲,採取合適的幀率。客戶端取消快取也可以減少首個關鍵幀顯示時間。具體參考首屏秒開和追幀播放技術。

附上iOS和Android對IJKPlayer設定。

iOS端:

- (IJKFFOptions *)options {
    if (!_options) {
        IJKFFOptions *options = [IJKFFOptions optionsByDefault];
        // Set param
        [options setFormatOptionIntValue:1024 * 16 forKey:@"probsize"];
        [options setFormatOptionIntValue:50000 forKey:@"analyzeduration"];
        [options setPlayerOptionIntValue:0 forKey:@"videotoolbox"];
        [options setCodecOptionIntValue:IJK_AVDISCARD_DEFAULT forKey:@"skip_loop_filter"];
        [options setCodecOptionIntValue:IJK_AVDISCARD_DEFAULT forKey:@"skip_frame"];
        [options setPlayerOptionIntValue:1000 forKey:@"max_cached_duration"];
        [options setPlayerOptionIntValue:1 forKey:@"infbuf"];  // 無限讀
        [options setPlayerOptionIntValue:0 forKey:@"packet-buffering"];
        _options = options;
    }
    return _options;
}

Android端:

// 設定播放前的最大探測時間
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100L);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240L);
// 每處理一個packet之後重新整理io上下文
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1L);
// 是否開啟預緩衝,一般直播專案會開啟,達到秒開的效果,不過帶來了播放丟幀卡頓的體驗
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0L);
// 放前的探測Size,預設是1M, 改小一點會出畫面更快
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "probsize", 200);
// 設定播放前的探測時間 1,達到首屏秒開效果
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 1000);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);
// 無限讀
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", 0);
// 不額外優化(使能非規範相容優化,預設值0 )
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "fast", 1);
// 縮短播放的rtmp視訊延遲在1s內
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
// 如果是rtsp協議,可以優先用tcp(預設是用udp)
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtmp_transport", "tcp");
// 支援硬解 1:開啟 O:關閉
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-hevc", 1);
// 跳幀處理,放CPU處理較慢時,進行跳幀處理,保證播放流程,畫面和聲音同步
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp");
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1L);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http-detect-range-support", 0);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48L);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_frame", 0);
// 因為專案中多次呼叫播放器,有網路視訊,resp,本地視訊,還有wifi上http視訊,所以得清空DNS才能播放WIFI上的視訊
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_clear", 1);

編譯之前修改f_ffplay.c,該方法明顯提高了首屏延遲問題

路徑 ijkmedia—> ijkplayer —> ff_ffplay.c

第一個修改的地方:double vp_duration 方法

將此程式碼

static double vp_duration(VideoState *is, Frame *vp, Frame *nextvp) {
    if (vp->serial == nextvp->serial) {
        double duration = nextvp->pts - vp->pts;
        if (isnan(duration) || duration <= 0 || duration > is->max_frame_duration)
            return vp->duration;
        else
            return duration;
    } else {
        return 0.0;
    }
}

替換為一下程式碼

static double vp_duration(VideoState *is, Frame *vp, Frame *nextvp) {
     return vp->duration;
}

第二個修改的地方:static int ffplay_video_thread(void *arg) 方法

註釋掉AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);這一行程式碼

duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);

修改為duration = 0.01;

static int ffplay_video_thread(void *arg)
{
    FFPlayer *ffp = arg;
    VideoState *is = ffp->is;
    AVFrame *frame = av_frame_alloc();
    double pts;
    double duration;
    int ret;
    AVRational tb = is->video_st->time_base;
  	// 註釋掉
    // AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
    int64_t dst_pts = -1;
    int64_t last_dst_pts = -1;
    int retry_convert_image = 0;
    int convert_frame_count = 0;
  
    // ···此處省略很多程式碼
  	
#endif
						// 這行程式碼直接修改為 duration = 0.01;
						// duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
            duration = 0.01;
            pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
            ret = queue_picture(ffp, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
            av_frame_unref(frame);
#if CONFIG_AVFILTER
        }
#endif

        if (ret < 0)
            goto the_end;
    }
 the_end:
#if CONFIG_AVFILTER
    avfilter_graph_free(&graph);
#endif
    av_log(NULL, AV_LOG_INFO, "convert image convert_frame_count = %d\n", convert_frame_count);
    av_frame_free(&frame);
    return 0;
}

修改f_ffplay.c參考了部落格ijkplayer的一些問題優化記錄