開發需求html
剛不久開發一款了教育類app,須要實現教師端對學生移動設備進行遠程操控,好比對學平生板進行解鎖屏,共享電腦屏幕到學生端,監控學生屏幕內容等。java
網絡環境ios
教師端網線或WIFI接入,iPad和Android Pad經過WIFI接入,確保在一個網段下。nginx
大體功能git
教師端採用FFmpeg採集屏幕音視頻,iOS、Android端使用ijkplayer拉流播放,流傳輸協議採用RTMP協議,通信方式採用TCP Socket。github
通信實現objective-c
局域網內教師端充當服務器發送UDP廣播(內容包含本機IP和端口號),iOS、Android端收到UDP廣播獲取到IP地址和端口後,採用Socket【CocoaAsyncSocket(iOS)、Socket(Android)】與教師端進行TCP鏈接,創建鏈接完成後,經過Socket收發消息進行通信。緩存
1.Homebrew安裝ruby
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
2.安裝nginx-full(rtmp)bash
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
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
參考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
iOS 編譯可能會遇到的問題和解決辦法
問題一:
./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"
問題二:
'openssl/ssl.h' file not found #include <openssl/ssl.h> ERROR: openssl not found
解決辦法:
編譯ffmpeg軟解碼庫,這個過程會生成各類架構的ffmpeg,編譯ffmpeg前要先compile OpenSSL,對openssl進行編譯,若是未執行可能會報錯。必須先執行./compile-openssl.sh all
實際編譯的確會遇到這些問題,尤爲是問題一。
這些問題參考了博客iOS IJKPlayer 項目集成
一切順利完成後運行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就是合併後的動態庫,直接拖拽到項目使用
個人Xcode版本 Version 10.3 (10G8)
首屏秒開,須要結合視頻清晰度和延遲,採起合適的幀率。客戶端取消緩存也能夠減小首個關鍵幀顯示時間。具體參考首屏秒開和追幀播放技術。
附上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的一些問題優化記錄