2020年04月29日

RaspberryPi4でyoutube liveに配信するスクリプト

Raspberry Pi 4を使ってRTSPで配信される動画をRTMPに変換してyoutube liveで配信するシェルスクリプトです。

Webカメラ

Webカメラには、IO-Dataの屋外WebカメラのTS-NA200Wを使用しています。
AC100Vが50Hzの地域では25fpsで1920x1080の動画を撮影する事が可能です。ビットレートはCBRで2Mbpsになります。これを無線LANで転送する事が可能です。

Raspberry Pi 4

Raspberry Pi 4にRaspbianをインストールして使用しています。インストールしたパッケージは一番ミニマルなパッケージになります。

pi@raspberrypi:~ $ uname -a
Linux raspberrypi 4.19.97-v7l+ #1294 SMP Thu Jan 30 13:21:14 GMT 2020 armv7l GNU/Linux

ffmpegの設定

ffmpegでTS-NA200WからRTSPでパケットを受け、ffmpegで現在時刻等の字幕を合成して、RTMPでyoutube liveに送信をします。
ffmpegにはハードウェアエンコーダが搭載されており、Raspbianでも使用することが可能です。ffmpegでハードウェアエンコーダを指定するにはエンコーダーにh264_omxを指定することで可能となります。
ハードウェアエンコーダを使用して変換をしましたが、CPU使用率は抑えることができましたが、fpsを稼ぐことができずリアルタイムエンコードが難しい状況でした。そのため、4コアあるCPUを活用する方針としてCPUエンコーダーであるlibx264を使用しました。

また、youtube liveの仕様で音声ストリームが無い動画は受け付けないようだったので、無音の音声を追加して配信をしています。

#!/bin/sh

#=====CONFIG=====
# youtubeで指定されたlivekeyを設定する
LIVEKEY="xxxx-xxxx-xxxx-xxxx"
# ライブ配信を開始した時刻を設定する
STREAMING_START_TIME=`date +%s -d '2020-4-21 19:38:0'`

#=====URL_SETTING=====
RTSP_URL="rtsp://userid:password@192.168.1.x:16272/ipcam_h264.sdp"
RTMP_URL="rtmp://a.rtmp.youtube.com/live2/${LIVEKEY}"

#=====MAIN=====
while true
do
  echo "==========Start[`date +%Y%m%d_%H%M%S`]=========="

  ELAPSED_TIME=$((`date +%s` - ${STREAMING_START_TIME}))
  TEXT="drawtext=fontfile=./ipagp.ttf: text='gaso / Current Time %{localtime\:%F %T} JST (UTC+9) / Elapsed Time %{pts\:hms\:$ELAPSED
_TIME}': fontcolor=white@0.7: fontsize=32: bordercolor=black: borderw=3: x=8: y=8'"

  #FFMPEG OPTION
  # -thread 0
  #  ffmpegのスレッド数を0(最適)に設定する
  # -re
  #  入力ストリームのフレームレート速度で読み込む
  #  (TS-NA220Wの出力フレームレートは25pfsに設定)
  # -rtsp_transport tcp
  #  RTSP transport protcolにTCPを使用する
  #  デフォルト値のUDPを使用するとパケットロスが発生しブロックノイズが発生する
  # -i $RTSP_URL
  #  入力ストリームの指定
  #  IDとpasswordはIPアドレスの前に指定する
  # -filter_complex "$TEXT"
  #  動画に表示する現在時刻や経過時間などを合成するためのフィルター
  #  filter_complexは複数入力ストリームを扱えるが今回はRTPSの入力ソース1つだけを使用
  # -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100
  #  仮想デバイスから無音の音声を生成する
  # -c:a aac
  #  音声はaacコーデックで変換する
  # -f flv
  #  出力フォーマットはyoutubeが指定するFLVとする
  # -loglevel info
  #  ログレベルを指定する
  #  動作状況を確認する時はinfo、本番稼働時はfatalとする
  # -b:v 5000k
  #  出力ビットレートを5Mbpsに指定する
  #  youtubeの推奨値は1920x1080で3500kbpsとされているがyoutube側で再変換する際にビットレートが低いと画質劣化が生じる
  # -c:v libx264
  #  エンコーダーをlibx264に指定する
  #  raspberry pi 4はハードウェアエンコーダh264_omxが使用できるが4coreのCPUで計算する方がパフォーマンスが良かった
  # -preset ultrafast
  #  圧縮率の指定を最も圧縮率の悪いultrafastに指定する
  #  帯域に余裕があるのであれば圧縮率を上げる必要はない
  #  ultrafast以外のsuperfast, veryfast等ではリアルタイム処理が間に合わない
  # -force_key_frames expr:gte\(t,n_forced*4\)
  #  キーフレームを4秒に1回入れるように指定する
  #  youtubeが2から4秒毎にキーフレームを入れるよう推奨している
  # $RTMP_URL
  #  出力ストリームを指定する
  ffmpeg -threads 0 -re -rtsp_transport tcp -i $RTSP_URL -filter_complex "$TEXT" -f lavfi -i anullsrc=channel_layout=stereo:sample_r
ate=44100 -c:a aac -f flv -loglevel info -b:v 5000k -c:v libx264 -preset ultrafast -force_key_frames expr:gte\(t,n_forced*4\) $RTMP_
URL

  #=====WAITING TIME=====
  echo "==========Finished[`date +%Y%m%d_%H%M%S`]=========="
  sleep 2
done

スクリプトの動作にあたっては、スクリプトのあるパスと同じ場所にフォントファイルが必要になります。このスクリプトではIPAフォントを使用しました。

パフォーマンス

ロードアベレージは4を超える事もあります。概ねCPU使用率は300%から350%程度でした。

pi@raspberrypi:~ $ more hoge
top - 19:01:47 up 15:30,  2 users,  load average: 4.08, 3.25, 1.71
Tasks: 107 total,   1 running, 106 sleeping,   0 stopped,   0 zombie
%Cpu(s): 28.1 us,  4.7 sy, 57.8 ni,  9.4 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :   3728.0 total,   3339.5 free,    221.3 used,    167.2 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.   3384.4 avail Mem

CPUを酷使するためCPU温度はかなり高温になります。4月の気温でもCPUにヒートシンクを付け小型のファンを付けた状態で温度は60度を越えました。

pi@raspberrypi:~ $ vcgencmd measure_temp
temp=61.0'C
2014年07月12日

Raspberry Piでロータリーエンコーダーの動作確認

Raspberry Piで秋月電子で販売されているロータリーエンコーダー(EC12PLRGBSDVBF-D-25K-24-24C-61)の動作確認を行いました。

ロータリーエンコーダーの接続

ロータリーエンコーダーとは以下の通り接続を行いました。GPIOはプログラムでプルアップしています。

[RE]  - [RaspberryPi]
Cpin  -  14pin(GND)
Apin  -  11pin(GPIO17)
Bpin  -  12pin(GPIO18)

プログラミング

プログラムではwiringPiのwiringPiISR関数を使用しました。この関数はGPIOのレベルが変化した際に指定した関数を呼び出す事ができ、ロータリーエンコーダーの回転に合わせたイベント処理が可能となります。
また、使用したロータリーエンコーダーはクリック機能が付いています。GPIOをプルアップすることにより、何もしていない時はA端子・B端子共にLレベルとなります。回転させるとA端子とB端子がそれぞれHレベルになり、次のクリック位置になると両方ともにLレベルに戻ります。右回転と左回転を識別するには、A端子とB端子のイベントが起こる順番を識別することで可能となります。

#include <wiringPi.h>
#include <stdio.h>
#include <stdlib.h>

int a;
int b;
int p_a;
int p_b;
int num;

void click_a(void){
        int _a;
        _a = digitalRead(0);//GPIOの値を取得。
        if(_a != a){ //同じ値が連続した場合にスキップする。
                if(a == 1){ //一つ前のA端子の値を格。
                        p_a = 1;
                }else{
                        p_a = 0;
                }
                a = _a;
                //a端子とb端子の直前の値が1であり、今の値が0である場合にTRUE。
                if(a == 0 && b == 0 && p_a == 1 && p_b == 1){
                        printf("right %d\n", ++num);
                }
        }
}
void click_b(void){
        int _b;
        _b = digitalRead(1);
        if(_b != b){
                if(b == 1){
                        p_b = 1;
                }else{
                        p_b = 0;
                }
                b = _b;
                if(a == 0 && b == 0 && p_a == 1 && p_b == 1){
                        printf("left  %d\n", --num);
                }
        }
}

int main(void){
        int setup = 0;
        a=1;
        b=1;
        p_a=0;
        p_b=0;
        num=0;
        setup = wiringPiSetup();
        pullUpDnControl(0, PUD_UP);
        pullUpDnControl(1, PUD_UP);
        wiringPiISR(0, INT_EDGE_BOTH, click_a);
        wiringPiISR(1, INT_EDGE_BOTH, click_b);
        while(setup != -1){
                sleep(1);
        }
}
2014年07月06日

Raspberry Piで大気圧センサ(AE-SCP1000-D01)の動作確認

Raspberry Piと秋月電子で販売されている大気圧センサーのSCP1000を使用して大気圧計を作成しました。

SPIの有効化

使用したLinuxはdebianで、以下のバージョンです。

pi@raspberrypi ~/work/lcd $ uname -a
Linux raspberrypi 3.12.22+ #691 PREEMPT Wed Jun 18 18:29:58 BST 2014 armv6l GNU/Linux

SPIを有効化するにはraspi-configコマンドを使用します。

pi@raspberrypi ~/work/lcd $ sudo raspi-config

[8 Advanced Options -> A5 SPI -> Yes]

SCP1000の接続

SCP1000はSPIインターフェースを備えています。以下の通り配線を行いました。どちらも物理ピン番号を記載しています。

[scp1000]       -     [RaspberryPi]
 3pin(SCK)      -      23pin(SCKL)
 4pin(GND)      -      25pin(GND)
 5pin(MOSI) - 0.01uF - 19pin(MOSI)
 6pin(MISO)     -      21pin(MISO)
 7pin(CSB)      -      24pin(CE0)
 8pin(VCC)      -      17pin(3.3v)

本来、上記接続で問題なく接続できるはずですが、Raspberry Piからどんなコマンドを送っても、読み出すと0x00又は0xFFの値しか返ってこない状態となりました。そこで、SCP1000側のMOSIの近くに0.01uFを挟むと4回に1回程度ですが信号が届くようになりました。原因は不明ですが、ブレッドボードで接続しているため、ノイズ等の影響を受けて不安定になっている可能性があります。

SC2004CSWB-XA-LB-Gの接続

表示デバイスは、秋月電子で販売されているSC2004CSWB-XA-LB-Gを使用しました。このデバイスは、4行x20桁が表示でき、バックライトに白色LEDを備えています。接続は以下の通り行っています。

[LCD]         -     [RaspberryPi]
 1pin(VSS)    -      3pin(GND)
 2pin(VDD)    -      1pin(3.3v)
 3pin(Vo)  - 2kΩ -  14pin(GND)
 4pin(RS)     -      16pin(GPIO23)
 5pin(R/W)    -      5pin(GND P5-Header)
 6pin(E)      -      18pin(GPIO24)
 11pin(DB4)   -      11pin(GPIO17)
 12pin(DB5)   -      12pin(GPIO18)
 13pin(DB6)   -      13pin(GPIO21)
 14pin(DB7)   -      15pin(GPIO22)
 
[LCD LED]  - [Raspberry Pi]
 A         -  2pin(5v)
 K - 180Ω -  20pin(GND)

なお、コントラスト調整用のボリュームは測定すると1.98kΩでした。
また、LEDの駆動電源は5Vを用いています。Raspberry Piの3.3vピンは電流の制限値があることから、USBコネクタと直接つながっている5Vを使用し、制限抵抗は180Ωを用いました。

プログラミング

動作が安定しなかったため初期化に失敗した場合は、再試行を繰り返す動作にしています。一度でも正しく値を送信できると、極めて安定して気圧データを取得することができました。MOSIにコンデンサを挟むことで安定性が多少増しましたが、以下のコードの初期化の処理が誤っている可能性もあります。参考程度に留めておいて下さい。

#include<wiringPiSPI.h>
#include<wiringPi.h>
#include<stdio.h>
#include<time.h>
#include<lcd.h>

#define SPI_CHANNEL 0
#define SPI_CLK 500000

int main(void){
	double temp = 0;
	double pressure = 0;
	unsigned char buf[3];
	char str[24];
	time_t time_now;
	struct tm *local_time;
	int i,fd,flag1=0, flag2=0;

        //wiringPiライブラリの初期化
        if (wiringPiSetup () == -1){
                return (1) ;
	}

        /* LCDデバイスの初期化 */
        if((fd = lcdInit(4,20,4, 4,5, 0,1,2,3,0,0,0,0)) < 0){
                printf("[Error] open lcd device\n");
                return(-1);
        }

	//SPIデバイスの初期化
	if((wiringPiSPISetup(SPI_CHANNEL, SPI_CLK))<0){
		printf("error SPI\n");
	}

	//初期処理。失敗した場合は繰り返す。
	while(1){
		flag1 = 0;
		flag2 = 0;

		//時間の表示
		time_now = time(NULL);
		local_time = localtime(&time_now);
		strftime(str, sizeof(str), "%Y/%m/%d %a %H:%M:%S", local_time);
		printf("%s\n", str);

		//ソフトリセットコード
		buf[0] = 0x1a;
		buf[1] = 0x01;
		wiringPiSPIDataRW(SPI_CHANNEL, buf, 2);
		printf("delay...\n");
		delay(60);

		//STATUS確認
		buf[0] = 0x1c;
		buf[1] = 0xff;
		wiringPiSPIDataRW(SPI_CHANNEL, buf, 2);
		printf("STATUS:%02x\n", buf[1]);
		i= buf[1]&0x01;
		if(i == 0){
			flag1 = 1;
			printf("STATUS OK!\n");
		}

		//DATARD8確認
		buf[0] = 0x7c;
		buf[1] = 0xff;
		wiringPiSPIDataRW(SPI_CHANNEL, buf, 2);
		printf("DATARD8:%02x\n", buf[1]);
		i= buf[1]&0x01;
		if(i == 1){
			flag2 = 1;
			printf("DATARD8 OK!\n");
		}

		if(flag1==1 && flag2==1){
			printf("START...\n");
			break;
		}
	}


	//オペレーションレジスタに高精度測定モードを設定
	buf[0] = 0x0e;
	buf[1] = 0x0a;
	wiringPiSPIDataRW(SPI_CHANNEL, buf, 2);

	//メインループ
	while(1){
		//温度の測定
		buf[0] = 0x84;
		buf[1] = 0x00;
		buf[2] = 0x00;
		wiringPiSPIDataRW(SPI_CHANNEL, buf, 3);
		temp = (double)((buf[1] << (8+2)) + (buf[2] << 2)) / 20 / 4;

		//気圧の測定
		buf[0] = 0x7c;
		buf[1] = 0x00;
		wiringPiSPIDataRW(SPI_CHANNEL, buf, 2);
		pressure = buf[1] << 16;
		buf[0] = 0x80;
		buf[1] = 0x00;
		buf[2] = 0x01;
		wiringPiSPIDataRW(SPI_CHANNEL, buf, 3);
		pressure += (buf[1] << 8) + buf[2];
		pressure = pressure / 4 / 100;

		//時間の取得
		time_now = time(NULL);
		local_time = localtime(&time_now);

		//LCD表示
       		lcdClear(fd);
	        lcdPosition(fd,0,0);
		strftime(str, sizeof(str), "%Y/%m/%d %a", local_time);
		lcdPrintf(fd, "DATE=%s", str);
	        lcdPosition(fd,0,1);
		strftime(str, sizeof(str), "%H:%M:%S", local_time);
		lcdPrintf(fd, "TIME=%s", str);
	        lcdPosition(fd,0,2);
       		lcdPrintf(fd, "TEMP=%2.2f[C]", temp);
	        lcdPosition(fd,0,3);
       		lcdPrintf(fd, "PRESS=%4.4f[hPa]", pressure);


		delay(1000);
	}
}

参考文献

合同会社パレットソフト:気圧センサ SCP1000 (試食)

2013年11月17日

FreeBSD上のbyoubuでの使用メモリの表示

byobuは、標準でロードアベレージやメモリ使用量などを表示することができますが、あくまでLinuxを対象に作成されているためFreeBSDではまったく情報を表示することができなくなります。そのため、自作のスクリプトを作成し表示を行うようにしました。

メモリ表示スクリプト

試しに、メモリの表示を行うスクリプト作成しました。FreeBSDはメモリの情報表示に関して、Linuxのように/procを読み込んで表示することはできません。sysctlを叩いて情報を取得します。

% more /usr/local/bin/mem
#!/bin/sh
a=`echo "4098*$(sysctl -n vm.stats.vm.v_free_count)/1024/1024" | bc`
b=`echo "$(sysctl -n sysctl hw.physmem)/1024/1024" | bc`
echo "${a}MB/${b}MB"

byobu設定ファイル

~/.byobu/profileの設定を変更します。
backtickが、どうやらバックグラウンドで表示を行うスクリプトと表示番号を定義している模様です。今回は、138と139という番号でスクリプトを作成しました。
hardstatus stringが、表示位置やレイアウトを設定している模様です。一番最後に138と139を追加しました。

backtick 138    5       5               mem
backtick 139    5       5               sysctl -n vm.loadavg

hardstatus string '%99`%{-}%{=r}%12` %100`%112`%=%117`%133`%130`%135`%102`%101`%129`%131`%127`%114`%115`%108`%134`%128`%125`%126`%113`%119`%116`%106`%104`%103`%105`%107`%136`%123`%137`%132`%120`%121` %138` %139`'

参考

/usr/local/lib/byobu/profile以下に各情報を取得するためのシェルスクリプトが格納されています。手っ取り早く表示させるならば、ここのスクリプトを書き換えてしまうのが、もっとも手軽です。

2013年10月13日

Raspberry PiでI2C温度計作成メモ

Raspberry Piと秋月電子で販売されている、ADT7410を使用して温度計を作成しました。前回作成した、NTP時計にセンサーを追加して動作するようにしています。

ADT7410の接続

ADT7410は、I2Cインターフェースを備えています。接続は大変簡単で、3.3VとGNDをRaspberry Piの1Pinと6Pinに差し、センサーのSCAとSDAをRaspberry Piの3Pin(SDA)と5Pin(SCA)に接続するだけで完了です。

Raspberry Piの準備

I2Cのモジュールを有効化します。まず、modprobeのブラックリストから、I2Cモジュールを除きます。

pi@raspberrypi ~ $ more /etc/modprobe.d/raspi-blacklist.conf
# blacklist spi and i2c by default (many users don't need them)

blacklist spi-bcm2708
#blacklist i2c-bcm2708

次に、moduleの有効化を行います。

pi@raspberrypi ~ $ more /etc/modules
# /etc/modules: kernel modules to load at boot time.
#
# This file contains the names of kernel modules that should be loaded
# at boot time, one per line. Lines beginning with "#" are ignored.
# Parameters can be specified after the module name.

snd-bcm2835
i2c-bcm2708
i2c-dev

最後にツールをインストールします。

pi@raspberrypi ~ $ sudo apt-get install i2c-tools

この状態で再起動すると、起動後にモジュールが有効化されます。

I2Cデバイスの確認

接続したI2Cデバイスが正しく認識されているかを確認します。ADT7410のデバイス番号は0x48なので、この値が表示されれば正しく接続されていることが分かります。

pi@raspberrypi ~ $ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- 48 -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

プログラミング

ADT7410とRaspberry PiのI2Cは、相性があまり良くないようです。詳細なバグ情報までは調査できませんでしたが、WiringPiのライブラリやi2cset/i2cgetコマンドを使用して読み書きができませんでした。やむを得ないので、単純にC言語で直接呼び出し使用することとしました。

読み込みに関しては、4byte単位であれば温度情報(0x00, 0x01)、ステータス情報(0x02)及び設定情報(0x03)を読み出すことが可能でした。
書き込みに関しては、2byte単位で、書き込みたいレジスタ(1byte)と設定したい値(1byte)を同時に書き込むことで設定可能でした。今回は、温度センサーの分解能を13bit動作から16bit動作に変更するために、センサー接続後にレジスタを設定しています。
いずれも、試したら動いたといった挙動で正しい制御を行っているかは分かりません。

#include <wiringPi.h>
#include <lcd.h>
#include <string.h>
#include <time.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <stdio.h>

#define DATA_NUM 10

double get_temp(int rtc){
        char buf[4];
        double temp;

        if (read(rtc, buf, 4) != 4) {buf[0] = 0x00; buf[1] = 0x00;}
        //temp = ((double)((int)buf[0] << 5) + ((int)buf[1] >> 3)) / 16; //13bitモード
        temp = ((double)((int)buf[0] << 8) + (int)buf[1]) / 128; //16bitモード
        return temp;
}

int main(void){
        int fd, rtc, i, count;
        char str[32];
        char REG[2] = {0x03,0x80}; //0x03レジスタに0x80を書き込む。
        time_t time_now;
        time_t time_prev;
        struct tm *local;
        double temp;
        double temp_arr[DATA_NUM];

        if (wiringPiSetup () == -1)
                return (1) ;

        /* LCDデバイスの初期化 */
        if((fd = lcdInit(2,16,4, 10,11, 2,3,4,5,0,0,0,0)) < 0){
                printf("[Error] open lcd device\n");
                return(-1);
        }


        /* LCD表示テスト */
        lcdClear(fd);
        lcdPosition(fd,0,0);
        lcdPrintf(fd, "NTP LCD_CLOCK");
        sleep(2);
        lcdClear(fd);


        /*i2c温度センサーの初期化*/
        if ((rtc = open("/dev/i2c-1", O_RDWR)) < 0){
                printf("[Error] open i2c port\n");
                return(-1);
        }
        if(ioctl(rtc, I2C_SLAVE, 0x48) < 0){
                printf("[Error] access to 0x48\n");
                return(-1);
        }
        if(write(rtc, REG, 2) != 2){
                printf("[Error] set register 0x03\n");
                return(-1);
        }


        /* 温度センサーの平均算出用データの取得 */
        for(i=0; i<DATA_NUM;i++){
                temp_arr[i] = get_temp(rtc);
                lcdClear(fd);
                lcdPosition(fd,0,0);
                lcdPrintf(fd, "Init.           TEMP_COUNT=%d/%d", i+1, DATA_NUM);
                sleep(1);
        }

        /* メインループ(1秒間隔で情報を更新) */
        count = 0;
        while(1){
                time_now = time(NULL);
                /* 時刻が前回時刻から変化した場合、表示更新処理を実行 */
                if(time_now != time_prev){
                        /* 時計表示ロジック */
                        local = localtime(&time_now);
                        strftime(str, sizeof(str), "%Y/%m/%d %a  %H:%M:%S", local);

                        /* 温度表示ロジック */
                        count = (count + 1) % DATA_NUM;
                        temp_arr[count] = get_temp(rtc);
                        temp = 0;
                        for(i=0;i<DATA_NUM;i++) temp += temp_arr[i];
                        temp = temp / DATA_NUM;

                        /* LCDへ表示 */
                        lcdPosition(fd,0,0);
                        lcdPrintf(fd, "%s %2.2fC", str, temp);
                        time_prev = time_now;

                        usleep(800*1000);//800msec処理を停止。上記処理が200msで終わらなくなったら修正。
                }
                usleep(1*1000);//1msec処理を停止
        }
        return(0);
}

参考文献

wsnakのブログ:I2C温度センサADT7410を使う(1)