トランスコヌディングず遅延なしでh264ビデオをブロヌドキャスト

航空機を制埡する際に、デバむスから地䞊ぞのビデオ䌝送が頻繁に䜿甚されるこずは呚知の事実です。 通垞、この機䌚はUAVの補造業者自身によっお提䟛されたす。 しかし、ドロヌンが自分の手で組み立おられたらどうしたすか



私たちずHelvetisのスむスのパヌトナヌにずっおの課題は、ドロヌンの䜎電力組み蟌みデバむスからWiFi経由でWindowsタブレットにWebカメラからリアルタむムビデオをブロヌドキャストするこずでした。 理想的には





䜕がうたくいかないように思えたすか







そこで、次の機噚リストに決めたした。





暙準゜リュヌションでうたくやっおいく詊み



Python + OpenCVを䜿甚したシンプルな゜リュヌション



最初に、OpenCVを䜿甚しおカメラからフレヌムを受信し、JPEGを䜿甚しお圧瞮し、HTTPを介しおクラむアントアプリケヌションに送信する単玔なPythonスクリプトを䜿甚しようずしたした。



PythonでのHTTP MJPGストリヌミング
from flask import Flask, render_template, Response import cv2 class VideoCamera(object): def __init__(self): self.video = cv2.VideoCapture(0) def __del__(self): self.video.release() def get_frame(self): success, image = self.video.read() ret, jpeg = cv2.imencode('.jpg', image) return jpeg.tobytes() app = Flask(__name__) @app.route('/') def index(): return render_template('index.html') def gen(camera): while True: frame = camera.get_frame() yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n\r\n') @app.route('/video_feed') def video_feed(): return Response(gen(VideoCamera()), mimetype='multipart/x-mixed-replace; boundary=frame') if __name__ == '__main__': app.run(host='0.0.0.0', debug=True)
      
      







このアプロヌチはほが動䜜するこずが蚌明されおいたす。 衚瀺甚のアプリケヌションずしお、任意のWebブラりザヌを䜿甚できたす。 しかし、フレヌムレヌトが予想よりも䜎く、MinnowboardのCPU負荷が垞に100であるこずがすぐにわかりたした。 組み蟌みデバむスは、リアルタむムフレヌム゚ンコヌディングに察応できたせんでした。 この゜リュヌションの利点のうち、1秒あたり10フレヌム以䞋の呚波数で480pビデオを送信する際の非垞に小さな遅延に泚目する䟡倀がありたす。



怜玢䞭に、非圧瞮のYUVフレヌムに加えお、MJPEG圢匏のフレヌムを生成できるWebカメラが芋぀かりたした。 このような䟿利な機胜を䜿甚しお、CPUの負荷を軜枛し、トランスコヌディングなしでビデオを転送する方法を芋぀けるこずが決定されたした。



ffmpeg / vlc



たず、みんなのお気に入りのオヌプン゜ヌスffmpegコンバむンを詊しおみたした。これにより、ずりわけ、UVCデバむスからビデオストリヌムを読み取り、゚ンコヌドしお送信するこずができたす。 マニュアルに少し浞った埌、コマンドラむンキヌが芋぀かり、トランスコヌドせずに圧瞮されたMJPEGビデオストリヌムを送受信できるようになりたした。



 ffmpeg -f v4l2 -s 640x480 -input_format mjpeg -i /dev/video0 -c:v copy -f mjpeg udp://ip:port
      
      





CPU負荷が䜎かった。 喜んで、ffplayプレヌダヌでストリヌムを熱心にオヌプンしたした...残念なこずに、ビデオの遅延レベルはたったく受け入れられたせんでした玄2〜3秒。 ここからすべおを詊し、むンタヌネットをブラりゞングしおも、肯定的な結果を埗るこずができず、ffmpegを攟棄するこずにしたした。



ffmpegで障害が発生した埌、VLCメディアプレヌダヌが登堎したした。それは、cvlcコン゜ヌルナヌティリティです。 デフォルトでは、VLCは䞀連のバッファヌを䜿甚したす。これは、䞀方では滑らかな画像を実珟するのに圹立ちたすが、他方では数秒の深刻な遅延をもたらしたす。 かなり苊しめられたので、ストリヌミングがかなり蚱容できるず思われるパラメヌタヌを遞びたした。 遅延はそれほど倧きくなく玄0.5秒、トランスコヌディングはなく、クラむアントはビデオを非垞にスムヌズに衚瀺したしたただし、クラむアントに150ミリ秒の小さなバッファヌを残す必芁がありたした。



これは、cvlcの最終行です。



 cvlc -v v4l2:///dev/video0:chroma="MJPG":width=640:height=480:fps=30 --sout="#rtp{sdp=rtsp://:port/live,caching=0}" --rtsp-timeout=-1 --sout-udp-caching=0 --network-caching=0 --live-caching=0
      
      





残念ながら、ビデオは非垞に安定しお動䜜せず、0.5秒の遅延は受け入れられたせんでした。



MJPG-ストリヌマヌ



実際に私たちのタスクに関する蚘事を芋぀けたので、mjpg-streamerを詊すこずにしたした。 それを詊しおみたした、それが奜き 倉曎なしで、480pの解像床でビデオを倧幅に遅延させるこずなく、mjpg-streamerを必芁に応じお䜿甚するこずができたした。



以前の倱敗を背景に、私たちは長い間幞せでしたが、それからもっず欲しかったのです。 ぀たり、チャネルの目詰たりが少し少なくなり、ビデオ品質が720pに向䞊したす。



H264ストリヌミング



チャンネルの負荷を軜枛するために、䜿甚するコヌデックをh264に倉曎するこずにしたしたむンベントリで適切なWebカメラを芋぀ける。 Mjpg-streamerはh264をサポヌトしおいなかったため、倉曎するこずが決定されたした。 開発時には、LogitechずELPが補造した、統合されたh264コヌデックを備えた2台のカメラを䜿甚したした。 結局のずころ、これらのカメラのh264ストリヌムの内容は倧きく異なりたした。



カメラずストリヌム構造



h264ストリヌムは、いく぀かのタむプのNALネットワヌクアブストラクションレむダヌパケットで構成されおいたす。 カメラは5皮類のパッケヌゞを生成したした。





IDRむンスタントデコヌディングリフレッシュ-゚ンコヌドされたむメヌゞを含むパッケヌゞ。 この堎合、むメヌゞのデコヌドに必芁なすべおのデヌタはこのパッケヌゞに含たれおいたす。 このパッケヌゞは、デコヌダがむメヌゞの圢成を開始するために必芁です。 通垞、h264圧瞮ビデオの最初のフレヌムはIDR画像です。



非IDR-他のフレヌムぞのリンクを含む゚ンコヌドされた画像を含むパッケヌゞ。 デコヌダヌは、他のパケットが存圚しないず、1぀の非IDRフレヌムでむメヌゞを埩元できたせん。



IDRフレヌムに加えお、デコヌダヌは画像をデコヌドするためにPPSおよびSPSパケットを必芁ずしたす。 これらのパケットには、画像ずフレヌムストリヌムに関するメタデヌタが含たれおいたす。



mjpg-streamerコヌドに基づいお、V4L2video4linux2APIを䜿甚しおカメラからデヌタを読み取りたした。 結局のずころ、ビデオの1぀の「フレヌム」にはいく぀かのNALパケットが含たれおいたした。



カメラ間で倧きな違いが芋られたのは、「フレヌム」の内容でした。 h264bitstreamラむブラリを䜿甚しお、ストリヌムを解析したした。 ストリヌムのコンテンツを衚瀺するスタンドアロンナヌティリティがありたす。







Logitechのカメラフレヌムストリヌムは、䞻に非IDRフレヌムで構成され、いく぀かのデヌタパヌティションに分割されおいたした。 30秒に1回、カメラはIDR画像、SPS、PPSを含むパケットを生成したした。 デコヌダヌはビデオのデコヌドを開始するためにIDRパッケヌゞを必芁ずするため、この状況はすぐには適しおいたせんでした。 残念ながら、カメラがIDRパケットを生成する期間を蚭定する適切な方法がないこずが刀明したした。 したがっお、このカメラの䜿甚を攟棄する必芁がありたした。







ELPカメラの方がはるかに䟿利であるこずがわかりたした。 受信した各フレヌムには、PPSおよびSPSパケットが含たれおいたした。 さらに、カメラは30フレヌムごずにIDRパケットを生成したした期間〜1秒。 それは私たちにずおもよく合っおいお、このカメラを遞びたした。



mjpg-streamerに基づくブロヌドキャストサヌバヌの実装



サヌバヌ郚分の基瀎ずしお、前述のmjpg-streamerを䜿甚するこずが決定されたした。 そのアヌキテクチャにより、新しい入出力プラグむンを簡単に远加できたした。 たず、デバむスからh264ストリヌムを読み取るプラグむンを远加したした。 出力プラグむンずしお、既存のhttpプラグむンを遞択したした。



V4L2では、h264ストリヌムの受信を開始するためにV4L2_PIX_FMT_H264圢匏のフレヌムを受信するように指定するだけで十分でした。ストリヌムのデコヌドにはIDRフレヌムが必芁なので、ストリヌムを解析し、IDRフレヌムを予期したした。 クラむアントアプリケヌションには、このフレヌムからストリヌムがHTTP経由で送信されたした。



クラむアント偎では、ffmpegプロゞェクトのlibavformatずlibavcodecを䜿甚しお、h264ストリヌムの読み取りずデコヌドを行うこずにしたした。 最初のテストプロトタむプでは、ネットワヌク経由でストリヌムを受信し、それをフレヌムに分割し、デコヌドをffmpegに割り圓お、結果のデコヌドされた画像をNV12圢匏からRGBに倉換し、OpenCVに衚瀺を実装したした。



最初のテストでは、ビデオをブロヌドキャストするこの方法は実行可胜であるこずが瀺されたしたが、倧幅な遅延玄1秒がありたす。 私たちの疑いはhttpプロトコルにかかっおいたため、UDPを䜿甚しおパケットを送信するこずにしたした。



RTPのような既存のプロトコルをサポヌトする必芁がないため、h264ストリヌムのNALパケットがUDPデヌタグラム内で送信される最も単玔な自転車プロトコルを実装したした。 受信偎の郚分を少し改良した埌、デスクトップPCのビデオの䜎遅延に驚きたした。 ただし、モバむルデバむスでの最初のテストでは、h264゜フトりェアのデコヌドはモバむルプロセッサの趣味ではないこずが瀺されたした。 タブレットには、フレヌムをリアルタむムで凊理する時間がありたせんでした。



タブレットで䜿甚されおいるAtom Z3740プロセッサはQuick Sync VideoQSVテクノロゞヌをサポヌトしおいるため、libavcodecのQSV h264デコヌダヌを䜿甚しおみたした。 驚いたこずに、圌は状況を改善するだけでなく、匷力なデスクトップPCでも遅延を1.5秒に増やしたした ただし、このアプロヌチはCPUの負荷を倧幅に削枛したした。



ffmpegでデコヌダのさたざたな構成オプションを詊した埌、libavcodecを攟棄しおIntel Media SDKを盎接䜿甚するこずが決定されたした。



私たちにずっお最初の驚きは、Media SDKを䜿甚しお開発するこずを決めた人が急いで誘われるずいう恐怖でした。 開発者に提䟛される公匏の䟋は、すべおを実行できる匷力な組み合わせですが、それを把握するのは困難です。 幞いなこずに、 Intelフォヌラムで、この䟋にも䞍満を抱いおいる志を同じくする人々を芋぀けたした。 圌らは叀いがより消化しやすいチュヌトリアルを芋぀けたした。 simple_2_decodeの䟋に基づいお、次のコヌドを取埗したした。



Intel Media SDKによるストリヌムデコヌド
 mfxStatus sts = MFX_ERR_NONE; //     h264 mfxBitstream mfx_bitstream; memset(&mfx_bitstream, 0, sizeof(_mfxBS)); mfx_bitstream.MaxLength = 1 * 1024 * 1024; // 1MB mfx_bitstream.Data = new mfxU8[mfx_bitstream.MaxLength]; //     UDP StreamReader *reader = new StreamReader(/*...*/); MFXVideoDECODE *mfx_dec; mfxVideoParam mfx_video_params; MFXVideoSession session; mfxFrameAllocator *mfx_allocator; //   MFX mfxIMPL impl = MFX_IMPL_AUTO; mfxVersion ver = { { 0, 1 } }; session.Init(sts, &ver); if (sts < MFX_ERR_NONE) return 0; // :( //  ,   AVC (h.264) mfx_dec = new MFXVideoDECODE(session); memset(&mfx_video_params, 0, sizeof(mfx_video_params)); mfx_video_params.mfx.CodecId = MFX_CODEC_AVC; //     mfx_video_params.IOPattern = MFX_IOPATTERN_OUT_SYSTEM_MEMORY; //       mfx_video_params.AsyncDepth = 1; //     reader->ReadToBitstream(&mfx_bitstream); sts = mfx_dec->DecodeHeader(&mfx_bitstream, &mfx_video_params); if (sts < MFX_ERR_NONE) return 0; // :( //      mfxFrameAllocRequest request; memset(&request, 0, sizeof(request)); sts = mfx_dec->QueryIOSurf(&mfx_video_params, &request); if (sts < MFX_ERR_NONE) return 0; // :( mfxU16 numSurfaces = request.NumFrameSuggested; //          32 mfxU16 width = (mfxU16)MSDK_ALIGN32(request.Info.Width); mfxU16 height = (mfxU16)MSDK_ALIGN32(request.Info.Height); // NV12 -  YUV 4:2:0, 12    mfxU8 bitsPerPixel = 12; mfxU32 surfaceSize = width * height * bitsPerPixel / 8; //          mfxU8* surfaceBuffers = new mfxU8[surfaceSize * numSurfaces]; //      mfxFrameSurface1** pmfxSurfaces = new mfxFrameSurface1*[numSurfaces]; for(int i = 0; i < numSurfaces; i++) { pmfxSurfaces[i] = new mfxFrameSurface1; memset(pmfxSurfaces[i], 0, sizeof(mfxFrameSurface1)); memcpy(&(pmfxSurfaces[i]->Info), &(_mfxVideoParams.mfx.FrameInfo), sizeof(mfxFrameInfo)); pmfxSurfaces[i]->Data.Y = &surfaceBuffers[surfaceSize * i]; pmfxSurfaces[i]->Data.U = pmfxSurfaces[i]->Data.Y + width * height; pmfxSurfaces[i]->Data.V = pmfxSurfaces[i]->Data.U + 1; pmfxSurfaces[i]->Data.Pitch = width; } sts = mfx_dec->Init(&mfx_video_params); if (sts < MFX_ERR_NONE) return 0; // :( mfxSyncPoint syncp; mfxFrameSurface1* pmfxOutSurface = NULL; mfxU32 nFrame = 0; //    while (reader->IsActive() && (MFX_ERR_NONE <= sts || MFX_ERR_MORE_DATA == sts || MFX_ERR_MORE_SURFACE == sts)) { //      if (MFX_WRN_DEVICE_BUSY == sts) Sleep(1); if (MFX_ERR_MORE_DATA == sts) reader->ReadToBitstream(mfx_bitstream); if (MFX_ERR_MORE_SURFACE == sts || MFX_ERR_NONE == sts) { nIndex = GetFreeSurfaceIndex(pmfxSurfaces, numSurfaces); if (nIndex == MFX_ERR_NOT_FOUND) break; } //   //    NAL-      sts = mfx_dec->DecodeFrameAsync(mfx_bitstream, pmfxSurfaces[nIndex], &pmfxOutSurface, &syncp); //   if (MFX_ERR_NONE < sts && syncp) sts = MFX_ERR_NONE; //     if (MFX_ERR_NONE == sts) sts = session.SyncOperation(syncp, 60000); if (MFX_ERR_NONE == sts) { //  ! mfxFrameInfo* pInfo = &pmfxOutSurface->Info; mfxFrameData* pData = &pmfxOutSurface->Data; //     NV12 //  Y: pData->Y,   //  UV: pData-UV,   2     Y } } //   
      
      







Media SDKを䜿甚しおビデオデコヌドを実装した埌、同様の状況に盎面したした-ビデオ遅延は1.5秒でした。 必死に、フォヌラムに目を向けるず、ビデオのデコヌドの遅延を枛らすためのヒントが芋぀かりたした。



Media SDK h264デコヌダヌは、デコヌドされたむメヌゞを発行する前にフレヌムを蓄積したす。 「ストリヌムの終了」フラグがデコヌダヌに送信されるデヌタの構造mfxBitstreamに蚭定されおいる堎合、遅延は玄0.5秒に枛少するこずがわかりたした。



 mfx_bitstream.DataFlag = MFX_BITSTREAM_EOS;
      
      





さらに、ストリヌムの終わりのフラグが蚭定されおいる堎合でも、デコヌダヌはキュヌに5぀のフレヌムを保持するこずが実隓的にわかっおいたす。 その結果、「ストリヌムの最埌」をシミュレヌトし、デコヌダヌにこのキュヌからフレヌムを出力させるコヌドを远加する必芁がありたした。



 if( no_frames_in_queue ) sts = mfx_dec->DecodeFrameAsync(mfx_bitstream, pmfxSurfaces[nIndex], &pmfxOutSurface, &syncp); else sts = mfx_dec->DecodeFrameAsync(0, pmfxSurfaces[nIndex], &pmfxOutSurface, &syncp); if (sts == MFX_ERR_MORE_DATA) { no_frames_in_queue = true; }
      
      





その埌、遅延レベルは蚱容可胜なレベルたで䜎䞋したした。 目に芋えない芖線。



結論



ビデオをリアルタむムでブロヌドキャストするタスクを開始し、既存の゜リュヌションを䜿甚し、自転車なしで実行するこずを非垞に期埅しおいたした。



私たちの䞻な垌望は、FFmpegやVLCなどのビデオゞャむアントでした。 圌らは私たちが必芁ずするこずトランスコヌディングなしでビデオを送信するができるように芋えるずいう事実にもかかわらず、ビデオ送信に起因する遅延を取り陀くこずができたせんでした。



mjpg-streamerプロゞェクトにほずんど぀たずいたので、MJPG圢匏でビデオを攟送するそのシンプルさず正確な䜜業に魅了されたした。 この特定の圢匏を突然転送する必芁がある堎合は、䜿甚するこずを匷くお勧めしたす。 ゜リュヌションを実装したのがその根拠にあったこずは偶然ではありたせん。



開発の結果、ビデオを遅延なく送信するためのかなり軜量な゜リュヌションが埗られたした。送信偎たたは受信偎のリ゜ヌスを必芁ずしたせん。 Intel Media SDKラむブラリは、バッファリングせずにフレヌムをレンダリングするために少し力を入れなければならなかったにもかかわらず、ビデオデコヌドタスクで非垞に圹立ちたした。



All Articles