[Kinesis Video Streams] パーサーライブラリ(kvssink)による動画送信で、解像度とフレームレートの調整でデータサイズが変化することを確認してみました。【RaspberryPi】

2020.02.02

1 はじめに

CX事業本部の平内(SIN)です。

Kinesis Video Streamsへの動画送信において、カメラを設置する環境によっては、通信帯域が気になることがあるかも知れません。

今回は、GStreamerに指定するパラメータで、解像度とフレームレートの調整することで、データサイズが、どのように変化(増減)するかを確認してみました。

通信帯域となると、データサイズだけでなく、送信時にフラグメントをまとめる際に付加するメタデータや、通信プロトコルのヘッダ等にも考慮が必要になりますが、すいません、ここでは、これらは考慮されていません。

2 環境

試した環境は、以下のとおりです。

Raspberry Piは、Model 4Bで、OSは、昨年9月の最新版(Raspbian GNU/Linux 10 (buster) 2019-09-26-raspbian-buster-full.img です。

$ cat /proc/cpuinfo  | grep Revision
Revision    : c03112

$ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 10 (buster)
Release:    10
Codename:   buster

$ uname -a
Linux raspberrypi 4.19.75-v7l+ #1270 SMP Tue Sep 24 18:51:41 BST 2019 armv7l GNU/Linux

USBカメラ(Logitech C920 HD Pro Webcam)をUSBで接続し、Raspberry Pi上のGStreamerでパーサーライブラリ(kvssink)を使用してKinesis Video Streamsへ送信しています。

転送される動画のサイズが、画像の圧縮率で変化するとややこしいので、画一になるようにと、とりあえず真っ黒な画面を送ることにしています。

3 GStreamerによる送信

下記のコマンドをベースに作業を進めます。

(1) 環境変数

以下のように、クレデンシャルとリージュンを環境変数で設定して、kvssinkのパラメータを簡略化しています。

$ export AWS_DEFAULT_REGION=ap-northeast-1
$ export AWS_ACCESS_KEY_ID=xxxxxxxxx
$ export AWS_SECRET_ACCESS_KEY=xxxxxxxxx

(2) GStreamer送信

動作確認するための基準となるコマンドラインを以下のとおりとしました。 v4l2srcで、USBカメラの入力を取得し、omxh264encでH.264エンコードしています。その後、h264parseでKinesis Video Stremasの規格に揃えて、kvssinkへ送っています。

解像度は、640✕480、フレームレートは、30/1となっています。

gst-launch-1.0 -v v4l2src device=/dev/video0 ! videoconvert ! video/x-raw,format=I420,width=640,height=480,framerate=30/1 ! omxh264enc control-rate=2 target-bitrate=512000 periodicity-idr=45 inline-header=FALSE ! h264parse ! video/x-h264,stream-format=avc,alignment=au,profile=baseline ! kvssink stream-name=TestStream 

4 保存されたフラグメント

送信されたデータは、Kinesis Data Streamsからフラグメント単位で取得が可能です。


Kinesis ビデオストリーム API およびプロデューサーライブラリのサポートより

下記のコードで、蓄積されているフラグメントの最後の5個を列挙しています。

コードの詳細については、下記の記事をご参照下さい。
参考:[Kinesis Video Streams] ストリーム上のフラグメントデータから、任意の時間帯を指定して動画を取得してみました。

const AWS = require('aws-sdk');
const kinesisvideo = new AWS.KinesisVideo();

async function listFragments(streamName, fragmentSelectorType, startTimestamp, endTimestamp) {
    var params = {
        APIName: "LIST_FRAGMENTS",
        StreamName: streamName
    };
    const e = await kinesisvideo.getDataEndpoint(params).promise();
    const kinesisvideoarchivedmedia = new AWS.KinesisVideoArchivedMedia({endpoint: e.DataEndpoint});

    const maxResults = 1000;
    let fragments = [];
    while(true){
        var nextToken;

        // listFragmentsのパラメータの生成(2回目以降は変換する)
        var params = { StreamName: streamName }
        if (nextToken) {
            params.NextToken = nextToken
        } else {
            params.FragmentSelector =  {
                FragmentSelectorType: fragmentSelectorType,
                TimestampRange: { 
                    EndTimestamp: endTimestamp, 
                    StartTimestamp: startTimestamp
                }
            }
            params.MaxResults =  maxResults;
        }

        // listFragmentsでフラグメント情報を取得する
        const data = await kinesisvideoarchivedmedia.listFragments(params).promise();
        data.Fragments.forEach( fragment => fragments.push(fragment) )

        // 次のデータがない場合は、列挙を終了する
        if(data.NextToken == null) {
            break;
        }
        nextToken = data.NextToken;
    }

    // 配列は、古い順にソートする
    return  fragments.sort((a,b)=> {
        if( a.ProducerTimestamp < b.ProducerTimestamp ) return -1;
        if( a.ProducerTimestamp > b.ProducerTimestamp ) return 1;
        return 0;
    })
}

async function job() {
    const streamName = 'TestStream';
    const fragmentSelectorType = "PRODUCER_TIMESTAMP";
    const startTimestamp = 0; // データの最初から取得する
    const endTimestamp = new Date(); // 現在の時間まで

    // フラグメントの列挙
    let fragments = await listFragments(streamName, fragmentSelectorType, startTimestamp, endTimestamp);

    // 最後の5つのフラグメントだけを列挙する
    const max = 5;
    for(var i=fragments.length-max; i<fragments.length; i++) {
        const fragment = fragments[i];
        const bytes = fragment.FragmentSizeInBytes;
        const msec = fragment.FragmentLengthInMilliseconds;
        const point = Math.round(bytes/msec * 10)/10;
        console.log(`FragmentLengthInMilliseconds: ${msec/1000} sec FragmentSizeInBytes: ${bytes} bytes [${point}]`);
    }
}

job();


先のコマンドでデータを送信して、コードを実行すると、下記のレスポンスを得ることができます。

FragmentLengthInMilliseconds: 2.929 sec FragmentSizeInBytes: 96936 bytes [33.1]
FragmentLengthInMilliseconds: 2.928 sec FragmentSizeInBytes: 97337 bytes [33.2]
FragmentLengthInMilliseconds: 2.929 sec FragmentSizeInBytes: 96911 bytes [33.1]
FragmentLengthInMilliseconds: 2.928 sec FragmentSizeInBytes: 96998 bytes [33.1]
FragmentLengthInMilliseconds: 2.928 sec FragmentSizeInBytes: 97158 bytes [33.2]

各行が、1つのフラグメントの情報であり、フラグメントに含まれるデータの時間とそのサイズです。最後の[]は、単純にデータサイズを時間で割った数値であり、雑ですが、1マイクロ秒のデータ量ってイメージです。

5 出力可能なフォーマットを確認する

USBカメラが出力可能なフォーマットは、gst-device-monitor-1.0で確認可能です。

$ gst-device-monitor-1.0 | grep video
    caps  : video/x-raw, format=(string)YUY2, width=(int)2304, height=(int)1536, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction)2/1;
            video/x-raw, format=(string)YUY2, width=(int)2304, height=(int)1296, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction)2/1;
            video/x-raw, format=(string)YUY2, width=(int)1920, height=(int)1080, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction)5/1;
            video/x-raw, format=(string)YUY2, width=(int)1600, height=(int)896, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)1280, height=(int)720, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 10/1, 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)960, height=(int)720, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 15/1, 10/1, 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)1024, height=(int)576, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 15/1, 10/1, 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)800, height=(int)600, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 24/1, 20/1, 15/1, 10/1, 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)864, height=(int)480, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 24/1, 20/1, 15/1, 10/1, 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)800, height=(int)448, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)640, height=(int)480, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)640, height=(int)360, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)432, height=(int)240, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)352, height=(int)288, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)320, height=(int)240, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)320, height=(int)180, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)176, height=(int)144, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)160, height=(int)120, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1 };
            video/x-raw, format=(string)YUY2, width=(int)160, height=(int)90, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1 };
        sysfs.path = /sys/devices/platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.3/1-1.3:1.0/video4linux/video0
        device.subsystem = video4linux
        device.path = /dev/video0
        v4l2.device.driver = uvcvideo           

表にしてみると以下のような感じです。(C920は、多数のフォーマットで出力が可能です)

width height frameeate
2304 1536 2/1
2304 1296 2/1
1920 1080 5/1
1600 896 15/2, 5/1
1280 720 10/1, 15/2, 5/1
960 720 15/1, 10/1, 15/2, 5/1
1024 576 15/1, 10/1, 15/2, 5/1
800 600 24/1, 20/1, 15/1, 10/1, 15/2, 5/1
864 480 24/1, 20/1, 15/1, 10/1, 15/2, 5/1
800 448 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1
640 480 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1
640 360 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1
432 240 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1
352 288 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1
320 240 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1
320 180 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1
176 144 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1
160 120 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1
160 90 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1

ここで列挙された組合せ以外を指定すると、Internal data stream error. となります。

以下は、解像度を640✕490に設定してエラーとなっている様子です。

$ gst-launch-1.0 -v v4l2src device=/dev/video0 ! videoconvert ! video/x-raw,format=I420,width=640,height=490,framerate=30/1 ! omxh264enc control-rate=2 target-bitrate=512000 periodicity-idr=45 inline-header=FALSE ! h264parse ! video/x-h264,stream-format=avc,alignment=au,profile=baseline ! kvssink stream-name=TestStream 
・・・略
ERROR: from element /GstPipeline:pipeline0/GstV4l2Src:v4l2src0: Internal data stream error.
・・・略

6 帯域の調整

いくつか、試してみた結果です。当然ですが、解像度が大きいほど、フレームレートが高いほど、データは大きくなっています。

640*480 30/1

FragmentLengthInMilliseconds: 2.929 sec FragmentSizeInBytes: 96936 bytes [33.1]
FragmentLengthInMilliseconds: 2.928 sec FragmentSizeInBytes: 97337 bytes [33.2]
FragmentLengthInMilliseconds: 2.929 sec FragmentSizeInBytes: 96911 bytes [33.1]
FragmentLengthInMilliseconds: 2.928 sec FragmentSizeInBytes: 96998 bytes [33.1]
FragmentLengthInMilliseconds: 2.928 sec FragmentSizeInBytes: 97158 bytes [33.2]

160*90 30/1

FragmentLengthInMilliseconds: 2.928 sec FragmentSizeInBytes: 16419 bytes [5.6]
FragmentLengthInMilliseconds: 2.928 sec FragmentSizeInBytes: 13588 bytes [4.6]
FragmentLengthInMilliseconds: 2.929 sec FragmentSizeInBytes: 14695 bytes [5]
FragmentLengthInMilliseconds: 2.928 sec FragmentSizeInBytes: 13970 bytes [4.8]
FragmentLengthInMilliseconds: 2.929 sec FragmentSizeInBytes: 12531 bytes [4.3]

640*480 15/1

FragmentLengthInMilliseconds: 5.521 sec FragmentSizeInBytes: 151543 bytes [27.4]
FragmentLengthInMilliseconds: 5.525 sec FragmentSizeInBytes: 152194 bytes [27.5]
FragmentLengthInMilliseconds: 5.52 sec FragmentSizeInBytes: 156854 bytes [28.4]
FragmentLengthInMilliseconds: 5.521 sec FragmentSizeInBytes: 154552 bytes [28]
FragmentLengthInMilliseconds: 5.521 sec FragmentSizeInBytes: 156240 bytes [28.3]

640*480 5/1

FragmentLengthInMilliseconds: 8.841 sec FragmentSizeInBytes: 155908 bytes [17.6]
FragmentLengthInMilliseconds: 8.838 sec FragmentSizeInBytes: 161728 bytes [18.3]
FragmentLengthInMilliseconds: 8.837 sec FragmentSizeInBytes: 166343 bytes [18.8]
FragmentLengthInMilliseconds: 8.837 sec FragmentSizeInBytes: 165101 bytes [18.7]
FragmentLengthInMilliseconds: 8.838 sec FragmentSizeInBytes: 165672 bytes [18.7]

1280*720 15/2

FragmentLengthInMilliseconds: 6.213 sec FragmentSizeInBytes: 412513 bytes [66.4]
FragmentLengthInMilliseconds: 6.293 sec FragmentSizeInBytes: 412509 bytes [65.6]
FragmentLengthInMilliseconds: 6.293 sec FragmentSizeInBytes: 412575 bytes [65.6]
FragmentLengthInMilliseconds: 6.289 sec FragmentSizeInBytes: 412579 bytes [65.6]
FragmentLengthInMilliseconds: 6.293 sec FragmentSizeInBytes: 412524 bytes [65.6]

7 最後に

今回は、v4l2srcからの入力を変えてみて、最終的にKinesis Data Streamsに蓄積されるデータサイズが、どのように変化するかを試してみました。

この確認は、通信帯域を正確に算定できているわけではありませんが、「トラフィックが何倍ぐらいになるか」というような目安を軽易に体感するぐらいはできるかも知れません。