ネットワークカメラからビデオをキャプチャする、パート2



最初の記事「オブジェクトまでの距離とその速度の測定」で、 Video4Linux2およびDirectXを使用したWebカメラからの画像のキャプチャについて検討しました。 次の記事「ネットワークカメラからのビデオのキャプチャ、パート1」では 、ネットワークMotion-JPEGカメラの使用方法を検討しました。 次に、ネットワークRTSPカメラ、特にRTSPを介したMotion-JPEGから画像をキャプチャする方法について説明します。



このタスクは、より多くのアクション、より多くの接続が必要であるため、Motion-JPEG over HTTPよりも複雑ですが、見返りに、柔軟性、速度、機能性、さらには何らかの汎用性が得られます。 正直なところ、RTSPは単純なタスクには冗長ですが、必要になる状況があることは間違いありません。



RTSPとは何ですか?



RTSPは、リアルタイムストリーミングプロトコル(リアルタイムストリーミングプロトコル)の略です。実際にはブロードキャスト制御プロトコルであり、「開始」、「停止」、「特定の時間への移行」などの複数のコマンドを実行できます。 このプロトコルは実装のHTTPに似ていますが、ヘッダーもあり、すべてがテキスト形式でも送信されます。 仕様からの主なコマンドは次のとおりです。

RTSPの特徴は、RTSP自体が必要なビデオデータを送信しないことです! プロトコル全体は通信専用です。 ここで、MVCとの類似性を見ることができます。データとその説明は分離されています。



主力製品は別のプロトコルです。RTP-リアルタイム転送プロトコル-リアルタイム転送プロトコル。 その助けにより、必要なデータが送信されます。 このプロトコルを使用することは非常に快適であり、データリンク層でのフラグメンテーション後にクライアントソフトウェアがデータを簡単に回復できることは注目に値します。 また、送信されるデータの形式、タイムスタンプ、同期フィールド(たとえば、オーディオとビデオが同時に送信される場合)など、いくつかの有用なフィールドがあります。 このプロトコルはTCPを介して機能しますが、速度指向のアプローチのため、通常UDPで使用されます。 つまり、RTPデータは、メディアコンテンツのヘッダーとペイロードデータを含むUDPデータグラムです。



他に何も必要ないようです。 RTSP経由で接続され、RTP経由でピックアップします。 しかし、そこには、スマートおじさんが3番目のプロトコル、 RTCP-リアルタイムトランスポートコントロールプロトコル-リアルタイムトランスポートコントロールプロトコルを思いつきました。 このプロトコルは、サービスの品質を判断するために使用され、その助けにより、クライアントとサーバーは、コンテンツの送信の良し悪しを知ることができます。 このデータに従って、サーバーは、たとえば、ビットレートを下げるか、別のコーデックに切り替えることができます。



RTPは偶数のポート番号を使用し、RTCPは次の奇数のポート番号を使用することが認められています。



RTSP通信の例


RTSPストリームのソースはeVidence APIX Box M1カメラのみであるため、すべての例はそれに関連しています。



以下は、VLCプレーヤー(これは私の研究で非常に役立ちます)とこのカメラの間の通信のログです。 VLCからカメラのポート554への最初の要求。 答えは空の行であり、「RTSP / 1.0」で始まります。



01: OPTIONS rtsp://192.168.0.254/jpeg RTSP/1.0 02: CSeq: 1 03: User-Agent: VLC media player (LIVE555 Streaming Media v2008.07.24) 04: 05: RTSP/1.0 200 OK 06: CSeq: 1 07: Date: Fri, Apr 23 2010 19:54:20 GMT 08: Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE 09: 10: DESCRIBE rtsp://192.168.0.254/jpeg RTSP/1.0 11: CSeq: 2 12: Accept: application/sdp 13: User-Agent: VLC media player (LIVE555 Streaming Media v2008.07.24) 14: 15: RTSP/1.0 200 OK 16: CSeq: 2 17: Date: Fri, Apr 23 2010 19:54:20 GMT 18: Content-Base: rtsp://192.168.0.254/jpeg/ 19: Content-Type: application/sdp 20: Content-Length: 442 21: x-Accept-Dynamic-Rate: 1 22: 23: v=0 24: o=- 1272052389382023 1 IN IP4 0.0.0.0 25: s=Session streamed by "nessyMediaServer" 26: i=jpeg 27: t=0 0 28: a=tool:LIVE555 Streaming Media v2008.04.09 29: a=type:broadcast 30: a=control:* 31: a=range:npt=0- 32: a=x-qt-text-nam:Session streamed by "nessyMediaServer" 33: a=x-qt-text-inf:jpeg 34: m=video 0 RTP/AVP 26 35: c=IN IP4 0.0.0.0 36: a=control:track1 37: a=cliprect:0,0,720,1280 38: a=framerate:25.000000 39: m=audio 7878 RTP/AVP 0 40: a=rtpmap:0 PCMU/8000/1 41: a=control:track2 42: 43: 44: SETUP rtsp://192.168.0.254/jpeg/track1 RTSP/1.0 45: CSeq: 3 46: Transport: RTP/AVP;unicast;client_port=41760-41761 47: User-Agent: VLC media player (LIVE555 Streaming Media v2008.07.24) 48: 49: RTSP/1.0 200 OK 50: CSeq: 3 51: Cache-Control: must-revalidate 52: Date: Fri, Apr 23 2010 19:54:20 GMT 53: Transport: RTP/AVP;unicast;destination=192.168.0.4;source=192.168.0.254;client_port=41760-41761; server_port=6970-6971 54: Session: 1 55: x-Transport-Options: late-tolerance=1.400000 56: x-Dynamic-Rate: 1 57: 58: SETUP rtsp://192.168.0.254/jpeg/track2 RTSP/1.0 59: CSeq: 4 60: Transport: RTP/AVP;unicast;client_port=7878-7879 61: Session: 1 62: User-Agent: VLC media player (LIVE555 Streaming Media v2008.07.24) 63: 64: RTSP/1.0 200 OK 65: CSeq: 4 66: Cache-Control: must-revalidate 67: Date: Fri, Apr 23 2010 19:54:20 GMT 68: Transport: RTP/AVP;unicast;destination=192.168.0.4;source=192.168.0.254;client_port=7878-7879; server_port=6972-6973 69: Session: 1 70: x-Transport-Options: late-tolerance=1.400000 71: x-Dynamic-Rate: 1 72: 73: PLAY rtsp://192.168.0.254/jpeg/ RTSP/1.0 74: CSeq: 5 75: Session: 1 76: Range: npt=0.000- 77: User-Agent: VLC media player (LIVE555 Streaming Media v2008.07.24) 78: 79: RTSP/1.0 200 OK 80: CSeq: 5 81: Date: Fri, Apr 23 2010 19:54:20 GMT 82: Range: npt=0.000- 83: Session: 1 84: RTP-Info: url=rtsp://192.168.0.254/jpeg/track1;seq=20730; rtptime=3869319494,url=rtsp://192.168.0.254/jpeg/track2;seq=33509;rtptime=3066362516 85: 86: #              87: 88: TEARDOWN rtsp://192.168.0.254/jpeg/ RTSP/1.0 89: CSeq: 6 90: Session: 1 91: User-Agent: VLC media player (LIVE555 Streaming Media v2008.07.24) 92: 93: RTSP/1.0 200 OK 94: CSeq: 6 95: Date: Fri, Apr 23 2010 19:54:25 GMT
      
      





VLCがカメラに最初に要求すること:

「私はあなたと何ができる?」 (オプション)

-こんにちは。 そして、オプション、説明、セットアップ、分解、再生、一時停止のいずれかを行うようにお願いできますか。

-では、「/ jpeg」の内容を教えてください。 (説明)

-ここには、最初のトラックが行くビデオ、M-JPEG、および2番目のトラックはオーディオシンプルです。

-ビデオ、最初のトラックを見て、ポケット番号41760で私に注いでください。ポケット番号41761の殻を捨てることができます。 (SETUPトラック1)

「OK、あなたの命令で...」

-そして、音を聞きたいのですが、7878、7879ポケットでの発疹。 (SETUPトラック2)

-はい、問題ありません。

-まあ、振りかけた。 (プレイ)

しばらくして:

-さて、十分、私は十分に見ました。 (分解)

-あなたが言うように。



ここで余談が終わります。 最初のリクエストでは、「 OPTIONS rtsp://192.168.0.254/jpeg RTSP/1.0



」は「 GET /jpeg HTTP/1.1



」に似ており、会話はこれで始まり、HTTPプロトコルにもOPTIONSメソッドがあります。 ここで、192.168.0.254はカメラのIPアドレスです。 CSeq



は要求のシーケンス番号を反映します。サーバーからの応答には同じCSeq



が含まれている必要があります。



サーバーからの応答は「 RTSP/1.0 200 OK



」で始まり、「 HTTP/1.1 200 OK



」のようになりHTTP/1.1 200 OK



すべてが正常であるという兆候です。リクエストは受け入れられ、リクエストは明確で、実装に問題はありませんでした。 また、プレーンテキストでは、使用可能なすべてのメソッドのリストが続きます。



次に、リクエスト/ jpegで私たちを待っている情報を収集します。これは、彼をフォローして、リンク「 rtsp://192.168.0.254/jpeg



」にrtsp://192.168.0.254/jpeg



です。 また、SDPの形式で回答を受け取りたいことを示します(行12)。



応答として、 Content-Type



およびContent-Length



示すRTSPヘッダーを取得し、ヘッダーの後に空の行を介して、SDP形式のコンテンツ自体が直接:



 v=0 o=- 1272052389382023 1 IN IP4 0.0.0.0 s=Session streamed by "nessyMediaServer" i=jpeg t=0 0 a=tool:LIVE555 Streaming Media v2008.04.09 a=type:broadcast a=control:* a=range:npt=0- a=x-qt-text-nam:Session streamed by "nessyMediaServer" a=x-qt-text-inf:jpeg m=video 0 RTP/AVP 26 c=IN IP4 0.0.0.0 a=control:track1 a=cliprect:0,0,720,1280 a=framerate:25.000000 m=audio 7878 RTP/AVP 0 a=rtpmap:0 PCMU/8000/1 a=control:track2
      
      





ここではすべてが明らかです。 次の行が必要です。



 #   m=video 0 RTP/AVP 26 #   RTP/AVP,  ,   26,   Motion-JPEG a=control:track1 #   a=cliprect:0,0,720,1280 #    a=framerate:25.000000 #       #   m=audio 7878 RTP/AVP 0 #  7878,    , 0 - PCM a=control:track2 #  
      
      





ビデオのみを受信する場合、オーディオデータからは、トラックの名前を除くすべてを無視します。 ストリームを設定するために必要ですが、このストリームを強制的に受け入れることはありませんが、オーディオを完全に無視するとカメラは動作を拒否します( SETUP



ビデオトラックに対してのみ行われる場合)。



正直に言うと、オーディオストリームのポート番号(7878)を無視すると、異なるカメラがどのように反応するかわかりません。これは、 SETUP



コマンドで指定するためです。



次に2つのSETUP



要求があり、ビデオおよびオーディオストリームを受信するポートを示します。 最初の番号はRTPのポート、2番目の番号はRTCPのポートです。 カメラの応答にはポートに関する情報が含まれています。ポートを参照して、すべてが正しく構成されていることを確認できます。 また、 Session



識別子を覚えておく必要があります。 後続のすべての呼び出しでそれを示す必要があります。



PLAY



コマンドの後、ビデオはポート41760に、オーディオはポート7878に転送され始めます。そしてTEARDOWN



コマンドTEARDOWN



ブロードキャストが停止し、接続が切断されます。



RTP上のMJPEG


RTPパケットが届くので、それらを解読する必要があります。 このために、すべてのフィールドの説明を含むこのようなパッケージの表をここに示します。

+ビットオフセット 0-1 2 3 4-7 8 9-15 16-31
0 V P X CC M PT シーケンス番号
32 タイムスタンプ
64 SSRC識別子
96 ... CSRC識別子...
96+(CC×32) 拡張ヘッダーID 拡張ヘッダー長(EHL)
96+(CC×32)+(X×32) ...拡張ヘッダー...
96+(CC×32)+(X×32)+(X×EHL) ペイロード


  1. V (バージョン):(2)プロトコルバージョン。 バージョン2になりました。
  2. P (パディング、サプリメント):(1)たとえば、暗号化アルゴリズムの場合、RTPパケットの最後に空のバイトが埋め込まれるときに使用されます。
  3. X (拡張):(1)は、アプリケーションによって決定される拡張ヘッダーの存在を示します。 私たちの場合、これは使用されません。
  4. CC (CSRCカウント):(4)CSRC識別子の数が含まれています。 私たちも使われていません。
  5. M (マーカー):(1)はアプリケーションレベルで使用されます。この場合、RTPパケットにJPEGフレームの終わりが含まれている場合、このビットは1に設定されます。
  6. PT (ペイロードタイプ):(7)は、ペイロードの形式-送信データを示します。 MJPEGの場合は26です。
  7. シーケンス番号 :(16)RTPパケット番号。失われたパケットの検出に使用されます。
  8. タイムスタンプ (32):タイムスタンプ、この場合は90,000ヘルツ(90000 = 1秒)。
  9. SSRC (Synchronization Source):(32)シンクロナイザーの識別子。ただし、とんでもないことに聞こえるかもしれません。 ストリームのソースを指定します。
  10. CSRC (Contributing Source):(32)複数の場所からのストリームがある場合に使用される追加ソースの識別子。
  11. Extension Header ID :(16)拡張機能の識別子。これがある場合は、それが何であるかを知る必要があります。 私たちの場合、それは使用されません。
  12. 拡張ヘッダーの長さ :(16)このヘッダーの長さ(バイト単位)。
  13. 拡張ヘッダー :タイトル自体。 コンテンツは、コンテキストに応じて非常に異なる場合があります。
  14. ペイロードペイロードデータは、まったく同じJPEGフレームです。 もちろん、断片化されています。
CSRCで始まるフィールドはオプションです。 私の知る限り、カメラからのMJPEGの送信には使用されません。



上記の1レベルのカプセル化を転送しました。 ここでのタスクは、受信したビデオデータを本格的なJPEGイメージに変換することです。 MJPEG over HTTPの場合、すべてが単純です-ストリームの一部を切り取り、JPEGイメージの場合と同様にすぐに操作します。 RTPの場合、画像は完全に送信されず、JPEGヘッダーはトラフィックを節約するために省略されます。 添付データとは別に復元する必要があります。



MJPEG仕様のRTPペイロードはRFC2435で説明されています 。 また、次の形式のすべてのフィールドの説明を記載した表を提供します。

+ビットオフセット 0-7 8-15 16-23 24-31
0 タイプ固有 フラグメントオフセット
32 種類 Q 身長
タイプ64..127の場合 マーカーヘッダーを再起動
Qが128..255の場合 MBZ 精度 長さ
量子化テーブルデータ


  1. タイプ固有タイプに依存):(8)フィールドの意味は実装に依存しますが、この場合は適用されません。
  2. フラグメントオフセット :(24)は、フレーム全体における現在のフレームフラグメントの位置を示します。
  3. タイプ :(8)イメージの復元方法はタイプによって異なります。
  4. Q (品質):(8)画質。
  5. :(8)フレーム幅。
  6. 高さ :(8)および高さ。
  7. Restart Marker header :(32)は、RSTマーカーが使用されている場合、JPEGのデコード時に使用されます。 カメラが使用されているかどうかはわかりませんが、このヘッダーは無視します。 このフィールドは、タイプ64〜127でのみ表示されます。
  8. 量子化テーブルデータ :存在する場合、それらを個別に計算する必要はありません。 また、JPEGデータから写真を適切に再作成するために必要です。 これらのテーブルが正しくない場合、画像は間違った色とコントラストになります。 2つのテーブルが必要です。輝度と色のそれぞれのルミナとクロマです。
  9. MBZ、精度、長さ :(32)量子化テーブルのパラメーター、私はそれらを無視し、長さを128-それぞれ64バイトの2つのテーブルに設定します。 そうでなければ、私は彼らと働く方法を知りません。
RSTマーカーヘッダーと量子化テーブルが存在しない場合があります。 最初のものがなければ、それは非常に良いことです。私は別のものを期待していないからです。 秒がない場合-必要なテーブルはパラメータQに基づいて計算されます。



RTCPパケットにはそのサブセットが含まれ、4つのタイプがあります。201-送信元レポート、202-受信者レポート、203-送信元の説明、204-送信先はアプリケーションによって決定されます。 まず、タイプ201を受け入れ、次にタイプ202を送信する必要があります。 203と204はオプションですが、それらも考慮に入れています。 1つのUDPパケットに複数のRTCPパケットが存在する場合があります。



すべてのタイプの構造は類似しています。 RTCPパケットは、次のデータで始まります。

+ビットオフセット 0-1 2 3-7 8-15 16-31
0 バージョン パディング SCまたはRCまたはサブタイプ パケットタイプ 長さ


  1. バージョン :(2)RTPバージョン。
  2. パディング :(1)RTPと同じ。
  3. SCまたはRCまたはサブタイプ :(5) タイプに応じて、受信者と送信元のレポートに含まれる送信元の数(送信元数)または受信者の数(受信者数)になります。 APPパケットの場合、このフィールドはそのようなパケットのサブタイプを定義します。
  4. パケットタイプ :(8)パケットタイプ、201-送信元レポート(送信者のレポートSS)、202-受信者レポート(受信者のレポートRR)、203-ソースの説明SDESおよび204-宛先はアプリケーション(APP)によって決定されます。
  5. 長さ :(16)ヘッダーに続くデータのサイズ。32ビット単位で測定されます。
さらに、各サブタイプのフィールドは提供しません。RFC3550で表示できます。 SSおよびRRタイプは、送信/受信パケットおよび時間遅延に関する情報を伝えるとだけ言っておきましょう。 SDES自体には、名前、電子メール、電話、場所など、ソースを定義するさまざまなテキストフィールドがあります。



これで紹介は終わりです。



RTSPクライアントを介したPython MJPEG



それでpythonに行きました。 クライアントはいくつかのファイルで構成され、 main.py



には受信した画像を処理するコールバック関数が含まれます。また、Twistedネットワークフレームワークメカニズムを開始し、カメラに接続するための設定を保存します。 私が短縮したすべてのリスト、フルバージョンは、記事の最後にあるリンクからダウンロードできます。

main.py

 20: def processImage(img): 21: 'This function is invoked by the MJPEG Client protocol' 22: # Process image 23: # Just save it as a file in this example 24: f = open('frame.jpg', 'wb') 25: f.write(img) 26: f.close() 27: 28: def main(): 29: print 'Python M-JPEG Over RSTP Client 0.1' 30: config = {'request': '/jpeg', 31: 'login': '', 32: 'password': 'admin', 33: 'ip': '192.168.0.252', 34: 'port': 554, 35: 'udp_port': 41760, 36: 'callback': processImage} 37: # Prepare RTP MJPEG client (technically it's a server) 38: reactor.listenUDP(config['udp_port'], rtp_mjpeg_client.RTP_MJPEG_Client(config)) 39: reactor.listenUDP(config['udp_port'] + 1, rtcp_client.RTCP_Client()) # RTCP 40: # And RSTP client 41: reactor.connectTCP(config['ip'], config['port'], rtsp_client.RTSPFactory(config)) 42: # Run both of them 43: reactor.run() 44: # On exit: 45: print 'Python M-JPEG Client stopped.'
      
      





原則として、RTCPプロトコルを実装せずにオーディオデータを受信することなく作業できます。 この場合、カメラは約1分後に切断されます。 常に再接続する必要があります。これは自動的に行われるため、問題は発生しません。 ただし、この記事では、RTCP部分を追加し、オーディオデータを受信するための空白を作成しました。



次に重要なファイルはrtsp_client.py



です。 彼は最も混乱していますが、彼の目標は明白です-上記の接続を正しく確立すること。

rtsp_client.py

 012: class RTSPClient(Protocol): 013: def __init__(self): 014: self.config = {} 015: self.wait_description = False 016: 017: def connectionMade(self): 018: self.session = 1 019: # Authorization part 020: if self.config['login']: 021: authstring = 'Authorization: Basic ' + b64encode(self.config['login']+':'+self.config['password']) + '\r\n' 022: else: 023: authstring = '' 024: # send OPTIONS request 025: to_send = """\ 026: OPTIONS rtsp://""" + self.config['ip'] + self.config['request'] + """ RTSP/1.0\r 027: """ + authstring + """CSeq: 1\r 028: User-Agent: Python MJPEG Client\r 029: \r 030: """ 031: self.transport.write(to_send) 032: if debug: 033: print 'We say:\n', to_send 034: 035: def dataReceived(self, data): 036: if debug: 037: print 'Server said:\n', data 038: # Unify input data 039: data_ln = data.lower().strip().split('\r\n', 5) 040: # Next behaviour is relevant to CSeq 041: # which defines current conversation state 042: if data_ln[0] == 'rtsp/1.0 200 ok' or self.wait_description: 043: # There might be an audio stream 044: if 'audio_track' in self.config: 045: cseq_audio = 1 046: else: 047: cseq_audio = 0 048: to_send = '' 049: if 'cseq: 1' in data_ln: 050: # CSeq 1 -> DESCRIBE 051: to_send = """\ 052: DESCRIBE rtsp://""" + self.config['ip'] + self.config['request'] + """ RTSP/1.0\r 053: CSeq: 2\r 054: Accept: application/sdp\r 055: User-Agent: Python MJPEG Client\r 056: \r 057: """ 058: elif 'cseq: 2' in data_ln or self.wait_description: 059: # CSeq 2 -> Parse SDP and then SETUP 060: data_sp = data.lower().strip().split('\r\n\r\n', 1) 061: # wait_description is used when SDP is sent in another UDP 062: # packet 063: if len(data_sp) == 2 or self.wait_description: 064: # SDP parsing 065: video = audio = False 066: is_MJPEG = False 067: video_track = '' 068: audio_track = '' 069: if len(data_sp) == 2: 070: s = data_sp[1].lower() 071: elif self.wait_description: 072: s = data.lower() 073: for line in s.strip().split('\r\n'): 074: if line.startswith('m=video'): 075: video = True 076: audio = False 077: if line.endswith('26'): 078: is_MJPEG = True 079: if line.startswith('m=audio'): 080: video = False 081: audio = True 082: self.config['udp_port_audio'] = int(line.split(' ')[1]) 083: if video: 084: params = line.split(':', 1) 085: if params[0] == 'a=control': 086: video_track = params[1] 087: if audio: 088: params = line.split(':', 1) 089: if params[0] == 'a=control': 090: audio_track = params[1] 091: if not is_MJPEG: 092: print "Stream", self.config['ip'] + self.config['request'], 'is not an MJPEG stream!' 093: if video_track: self.config['video_track'] = 'rtsp://' + self.config['ip'] + self.config['request'] + '/' + basename(video_track) 094: if audio_track: self.config['audio_track'] = 'rtsp://' + self.config['ip'] + self.config['request'] + '/' + basename(audio_track) 095: to_send = """\ 096: SETUP """ + self.config['video_track'] + """ RTSP/1.0\r 097: CSeq: 3\r 098: Transport: RTP/AVP;unicast;client_port=""" + str(self.config['udp_port']) + """-"""+ str(self.config['udp_port'] + 1) + """\r 099: User-Agent: Python MJPEG Client\r 100: \r 101: """ 102: self.wait_description = False 103: else: 104: # Do not have SDP in the first UDP packet, wait for it 105: self.wait_description = True 106: elif "cseq: 3" in data_ln and 'audio_track' in self.config: 107: # CSeq 3 -> SETUP audio if present 108: self.session = data_ln[5].strip().split(' ')[1] 109: to_send = """\ 110: SETUP """ + self.config['audio_track'] + """ RTSP/1.0\r 111: CSeq: 4\r 112: Transport: RTP/AVP;unicast;client_port=""" + str(self.config['udp_port_audio']) + """-"""+ str(self.config['udp_port_audio'] + 1) + """\r 113: Session: """ + self.session + """\r 114: User-Agent: Python MJPEG Client\r 115: \r 116: """ 117: reactor.listenUDP(self.config['udp_port_audio'], rtp_audio_client.RTP_AUDIO_Client(self.config)) 118: reactor.listenUDP(self.config['udp_port_audio'] + 1, rtcp_client.RTCP_Client()) # RTCP 119: elif "cseq: "+str(3+cseq_audio) in data_ln: 120: # PLAY 121: to_send = """\ 122: PLAY rtsp://""" + self.config['ip'] + self.config['request'] + """/ RTSP/1.0\r 123: CSeq: """ + str(4+cseq_audio) + """\r 124: Session: """ + self.session + """\r 125: Range: npt=0.000-\r 126: User-Agent: Python MJPEG Client\r 127: \r 128: """ 129: elif "cseq: "+str(4+cseq_audio) in data_ln: 130: if debug: 131: print 'PLAY' 132: pass 133: 134: elif "cseq: "+str(5+cseq_audio) in data_ln: 135: if debug: 136: print 'TEARDOWN' 137: pass 138: 139: if to_send: 140: self.transport.write(to_send) 141: if debug: 142: print 'We say:\n', to_send
      
      





オーディオトラックが存在する場合、このモジュールはrtp_audio_client.py



および対応するRTCPクライアントも起動します。



接続に成功すると、 rtp_mjpeg_client.py



作業に使用され、着信データストリームが処理されます。

rtp_mjpeg_client.py

 08: class RTP_MJPEG_Client(DatagramProtocol): 09: def __init__(self, config): 10: self.config = config 11: # Previous fragment sequence number 12: self.prevSeq = -1 13: self.lost_packet = 0 14: # Object that deals with JPEGs 15: self.jpeg = rfc2435jpeg.RFC2435JPEG() 16: 17: def datagramReceived(self, datagram, address): 18: # When we get a datagram, parse it 19: rtp_dg = rtp_datagram.RTPDatagram() 20: rtp_dg.Datagram = datagram 21: rtp_dg.parse() 22: # Check for lost packets 23: if self.prevSeq != -1: 24: if (rtp_dg.SequenceNumber != self.prevSeq + 1) and rtp_dg.SequenceNumber != 0: 25: self.lost_packet = 1 26: self.prevSeq = rtp_dg.SequenceNumber 27: # Handle Payload 28: if rtp_dg.PayloadType == 26: # JPEG compressed video 29: self.jpeg.Datagram = rtp_dg.Payload 30: self.jpeg.parse() 31: # Marker = 1 if we just received the last fragment 32: if rtp_dg.Marker: 33: if not self.lost_packet: 34: # Obtain complete JPEG image and give it to the 35: # callback function 36: self.jpeg.makeJpeg() 37: self.config['callback'](self.jpeg.JpegImage) 38: else: 39: #print "RTP packet lost" 40: self.lost_packet = 0 41: self.jpeg.JpegPayload = ""
      
      





彼は理解しやすい。 別のデータグラムを受け入れるたびに、 rtp_datagram.py



モジュールを使用して解析し、その結果をrfc2435jpeg.py



モジュールにrfc2435jpeg.py



ます。これにより、本格的なJPEGイメージが作成されます。 次に、 rtp_dg.Marker



マーカーが表示されるのを待ち、その表示が復元された画像でコールバック関数を呼び出す様子を確認します。



RTPデータグラムパーサーは次のようになります。

rtp_datagram.py

 26: def parse(self): 27: Ver_P_X_CC, M_PT, self.SequenceNumber, self.Timestamp, self.SyncSourceIdentifier = unpack('!BBHII', self.Datagram[:12]) 28: self.Version = (Ver_P_X_CC & 0b11000000) >> 6 29: self.Padding = (Ver_P_X_CC & 0b00100000) >> 5 30: self.Extension = (Ver_P_X_CC & 0b00010000) >> 4 31: self.CSRCCount = Ver_P_X_CC & 0b00001111 32: self.Marker = (M_PT & 0b10000000) >> 7 33: self.PayloadType = M_PT & 0b01111111 34: i = 0 35: for i in range(0, self.CSRCCount, 4): 36: self.CSRS.append(unpack('!I', self.Datagram[12+i:16+i])) 37: if self.Extension: 38: i = self.CSRCCount * 4 39: (self.ExtensionHeaderID, self.ExtensionHeaderLength) = unpack('!HH', self.Datagram[12+i:16+i]) 40: self.ExtensionHeader = self.Datagram[16+i:16+i+self.ExtensionHeaderLength] 41: i += 4 + self.ExtensionHeaderLength 42: self.Payload = self.Datagram[12+i:]
      
      





JPEGリカバリモジュールには、いくつかのテーブルとかなり長いヘッダー生成機能が含まれているため、十分に大きいです。 したがって、ここではそれらを省略し、RTPペイロードを解析して最終的なJPEGイメージを作成する機能のみを提供します。

rfc2435jpeg.py

 287: def parse(self): 288: HOffset = 0 289: LOffset = 0 290: # Straightforward parsing 291: (self.TypeSpecific, 292: HOffset, #3 byte offset 293: LOffset, 294: self.Type, 295: self.Q, 296: self.Width, 297: self.Height) = unpack('!BBHBBBB', self.Datagram[:8]) 298: self.Offest = (HOffset << 16) + LOffset 299: self.Width = self.Width << 3 300: self.Height = self.Height << 3 301: 302: # Check if we have Restart Marker header 303: if 64 <= self.Type <= 127: 304: # TODO: make use of that header 305: self.RM_Header = self.Datagram[8:12] 306: rm_i = 4 # Make offset for JPEG Header 307: else: 308: rm_i = 0 309: 310: # Check if we have Quantinization Tables embedded into JPEG Header 311: # Only the first fragment will have it 312: if self.Q > 127 and not self.JpegPayload: 313: self.JpegPayload = self.Datagram[rm_i+8+132:] 314: QT_Header = self.Datagram[rm_i+8:rm_i+140] 315: (self.QT_MBZ, 316: self.QT_Precision, 317: self.QT_Length) = unpack('!BBH', QT_Header[:4]) 318: self.QT_luma = string2list(QT_Header[4:68]) 319: self.QT_chroma = string2list(QT_Header[68:132]) 320: else: 321: self.JpegPayload += self.Datagram[rm_i+8:] 322: # Clear tables. Q might be dynamic. 323: if self.Q <= 127: 324: self.QT_luma = [] 325: self.QT_chroma = [] 326: 327: def makeJpeg(self): 328: lqt = [] 329: cqt = [] 330: dri = 0 331: # Use exsisting tables or generate ours 332: if self.QT_luma: 333: lqt=self.QT_luma 334: cqt=self.QT_chroma 335: else: 336: MakeTables(self.Q,lqt,cqt) 337: JPEGHdr = [] 338: # Make a complete JPEG header 339: MakeHeaders(JPEGHdr, self.Type, int(self.Width), int(self.Height), lqt, cqt, dri) 340: self.JpegHeader = list2string(JPEGHdr) 341: # And a complete JPEG image 342: self.JpegImage = self.JpegHeader + self.JpegPayload 343: self.JpegPayload = '' 344: self.JpegHeader = '' 345: self.Datagram = ''
      
      





rtp_audio_client.py



オーディオデータ受信モジュールも実装しましたが、再生可能なデータに変換しませんでした。 誰かがそれを必要とするならば、私はすべてがそうであるようにこのファイルでスケッチを作りました。 rfc2435jpeg.py



似た構文解析を行う必要があるだけです。 オーディオデータは断片化されていないため、簡単です。 各パッケージには、複製するのに十分なデータが含まれています。 この記事はすでに非常に長いので、ここではこのモジュールを取り上げません(すぐにHabrafoldを実装することになります)。



正しく動作するには、RTCPパケットを送受信し、送信者のレポートを受け入れ、受信者のレポートを送信する必要があります。 タスクを簡素化するために、カメラからSRを受信した直後にRRを送信し、すべてが正常であるという理想化されたデータを組み込みます。

rtcp_client.py

 09: class RTCP_Client(DatagramProtocol): 10: def __init__(self): 11: # Object that deals with RTCP datagrams 12: self.rtcp = rtcp_datagram.RTCPDatagram() 13: def datagramReceived(self, datagram, address): 14: # SSRC Report received 15: self.rtcp.Datagram = datagram 16: self.rtcp.parse() 17: # Send back our Receiver Report 18: # saying that everything is fine 19: RR = self.rtcp.generateRR() 20: self.transport.write(RR, address)
      
      





ただし、モジュールはRTCPデータグラムと直接連携します。 それも十分に大きかった。

rtcp_datagram.py

 049: def parse(self): 050: # RTCP parsing is complete 051: # including SDES, BYE and APP 052: # RTCP Header 053: (Ver_P_RC, 054: PacketType, 055: Length) = unpack('!BBH', self.Datagram[:4]) 056: Version = (Ver_P_RC & 0b11000000) >> 6 057: Padding = (Ver_P_RC & 0b00100000) >> 5 058: # Byte offset 059: off = 4 060: # Sender's Report 061: if PacketType == 200: 062: # Sender's information 063: (self.SSRC_sender, 064: self.NTP_TimestampH, 065: self.NTP_TimestampL, 066: self.RTP_Timestamp, 067: self.SenderPacketCount, 068: self.SenderOctetCount) = unpack('!IIIIII', self.Datagram[off: off + 24]) 069: off += 24 070: ReceptionCount = Ver_P_RC & 0b00011111 071: if debug: 072: print 'SDES: SR from', str(self.SSRC_sender) 073: # Included Receiver Reports 074: self.Reports = [] 075: i = 0 076: for i in range(ReceptionCount): 077: self.Reports.append(Report()) 078: self.Reports[i].SSRC, 079: self.Reports[i].FractionLost, 080: self.Reports[i].CumulativeNumberOfPacketsLostH, 081: self.Reports[i].CumulativeNumberOfPacketsLostL, 082: self.Reports[i].ExtendedHighestSequenceNumberReceived, 083: self.Reports[i].InterarrivalJitter, 084: self.Reports[i].LastSR, 085: self.Reports[i].DelaySinceLastSR = unpack('!IBBHIIII', self.Datagram[off: off + 24]) 086: off += 24 087: # Source Description (SDES) 088: elif PacketType == 202: 089: # RC now is SC 090: SSRCCount = Ver_P_RC & 0b00011111 091: self.SourceDescriptions = [] 092: i = 0 093: for i in range(SSRCCount): 094: self.SourceDescriptions.append(SDES()) 095: SSRC, = unpack('!I', self.Datagram[off: off + 4]) 096: off += 4 097: self.SourceDescriptions[i].SSRC = SSRC 098: SDES_Item = -1 099: # Go on the list of descriptions 100: while SDES_Item != 0: 101: SDES_Item, = unpack('!B', self.Datagram[off]) 102: off += 1 103: if SDES_Item != 0: 104: SDES_Length, = unpack('!B', self.Datagram[off]) 105: off += 1 106: Value = self.Datagram[off: off + SDES_Length] 107: off += SDES_Length 108: if debug: 109: print 'SDES:', SDES_Item, Value 110: if SDES_Item == 1: 111: self.SourceDescriptions[i].CNAME = Value 112: elif SDES_Item == 2: 113: self.SourceDescriptions[i].NAME = Value 114: elif SDES_Item == 3: 115: self.SourceDescriptions[i].EMAIL = Value 116: elif SDES_Item == 4: 117: self.SourceDescriptions[i].PHONE = Value 118: elif SDES_Item == 5: 119: self.SourceDescriptions[i].LOC = Value 120: elif SDES_Item == 6: 121: self.SourceDescriptions[i].TOOL = Value 122: elif SDES_Item == 7: 123: self.SourceDescriptions[i].NOTE = Value 124: elif SDES_Item == 8: 125: self.SourceDescriptions[i].PRIV = Value 126: # Extra parsing for PRIV is needed 127: elif SDES_Item == 0: 128: # End of list. Padding to 32 bits 129: while (off % 4): 130: off += 1 131: # BYE Packet 132: elif PacketType == 203: 133: SSRCCount = Ver_P_RC & 0b00011111 134: i = 0 135: for i in range(SSRCCount): 136: SSRC, = unpack('!I', self.Datagram[off: off + 4]) 137: off += 4 138: print 'SDES: SSRC ' + str(SSRC) + ' is saying goodbye.' 139: # Application specific packet 140: elif PacketType == 204: 141: Subtype = Ver_P_RC & 0b00011111 142: SSRC, = unpack('!I', self.Datagram[off: off + 4]) 143: Name = self.Datagram[off + 4: off + 8] 144: AppData = self.Datagram[off + 8: off + Length] 145: print 'SDES: APP Packet "' + Name + '" from SSRC ' + str(SSRC) + '.' 146: off += Length 147: # Check if there is something else in the datagram 148: if self.Datagram[off:]: 149: self.Datagram = self.Datagram[off:] 150: self.parse() 151: 152: def generateRR(self): 153: # Ver 2, Pad 0, RC 1 154: Ver_P_RC = 0b10000001 155: # PT 201, Length 7, SSRC 0xF00F - let it be our ID 156: Header = pack('!BBHI', Ver_P_RC, 201, 7, 0x0000F00F) 157: NTP_32 = (self.NTP_TimestampH & 0x0000FFFF) + ((self.NTP_TimestampL & 0xFFFF0000) >> 16) 158: # No lost packets, no delay in receiving data, RR sent right after receiving SR 159: # Instead of self.SenderPacketCount should be proper value 160: ReceiverReport = pack('!IBBHIIII', self.SSRC_sender, 0, 0, 0, self.SenderPacketCount, 1, NTP_32, 1) 161: return Header + ReceiverReport
      
      





解析はRFCに厳密に従っています。関数unpack



を使用してデータを数値変数に変換しoff



、現在のオフセットを含む変数使用してデータ配列を移動します。



また、リンクは次のとおりです。PythonMJPEG over RTSP client



ロシア語のコメント付きのリスティングのバージョンを作成することはできなくなったため、誰にとっても都合が悪い場合はご容赦ください。



読むのに便利

  1. インターネット上のマルチメディア
  2. オーディオとビデオのRTPプロファイルのリスト
この記事で終わり、そして誰がマスターした-よくやった!



All Articles