GoProからほぼリアルタイムの写真をWebサーバーから見る

ペリフェラル

GoproはOpen Gopro APIというAPIがあり、プログラムから操作できます。
最初にこのAPIを利用して写真を自動的に撮影するシステムを作りました。水中の今の状況をリモートで把握したいと考えるとビデオストリーミングが欲しくなります。

CV2を試す

とにかく理由のわからんビデオストリームを再生したいと思い、cv2を使ってRaspiのGUI画面に表示しました。(VLCのネットワークストリームでは再現できませんでした。)
注意点はGoproに一度、stopを送ってからstartしないと反応しません。(後述)

import requests
import cv2
import time

url = "http://10.5.5.9:8080/gopro/camera/stream/"
response = requests.get(url+"stop")
time.sleep(1)

querystring = {"port":"8556"}
response = requests.get(url+"start", params=querystring)
time.sleep(1)

cap = cv2.VideoCapture("udp://10.5.5.9:8556")

if (cap.isOpened() == False):
    print("Video open error.")
    exit(1) 

while True:
    k = cv2.waitKey(1)
    if k == 27: #esc key
        break
    ret,frame = cap.read()
    if not ret:
        continue

    cv2.imshow('Frame', frame)

 # get codec
fourcc_int = int (cap.get(cv2.CAP_PROP_FOURCC))
fourcc = list((fourcc_int.to_bytes(4,'little').decode('utf-8')))
print(fourcc)

cap.release()
cv2.destroyAllWindows()

しかしまぁ、タイムラグがすごい。10秒近くあります。なお、codec(動画圧縮規格)はこのプログラムの最後で調べていますが、h264のようです。

Goproからプレビューストリームを流すよう指示しなければいけません。以下のコマンドをGoProに送る必要があります。


import requests
import time
url="http://10.5.5.9:8080/gopro/camera/stream/"
response = requests.get(url+"stop") #必ず一度やること
time.sleep(1)
querystring = {"port":"8556"}
response = requests.get(url+"start", params=querystring)
time.sleep(1)

HLSを試す

次にライブストリーミングを試しました。Appleが提唱しているというHLSです。仕組みとしてはffmpegでGoproからのビデオストリームをHLSに変換し、ブラウザーから眺める形になります。
以下に設定した記録を書きますが、54秒で止まります。どこか設定が悪いのでしょう。しかしffmpegの設定パラメーターの多さに疲れ果てたのと、やはり遅延が10秒以上は軽くあるので断念した記録です。
肝心要のffmpegのコマンドです。動画系ってやったことないのでわかっていないことを告白しておきます。

#!/bin/bash

ffmpeg -re \
-codec:v h264 \
-i udp://10.5.5.9:8556 \
-an \
-probesize 50M \
-time_shift_buffer_depth 4 \
-preset superfast \
-vf scale=1024:-1 \
-hls_time 5 \
-g 15 \
-hls_list_size 5 \
-hls_playlist_type vod \
-hls_flags delete_segments \
-hls_segment_filename stream%03d.ts \
-f hls stream.m3u8つ

httpサーバーはlighthttpdをインストールしました。ドキュメント・ルートを自分のフォルダーに変更した以外はなにもconfは設定していません。mimeについてはhlsは登録されているようです。

htmlは以下のとおり。192.168.100.1はアクセスするRaspi側のアドレスです。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>HLS Streaming</title>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
</head>
<body>
<video id="video" controls></video>
<script>
var video = document.getElementById('video');
var videoSrc = 'http://192.168.100.1/stream.m3u8';

if (Hls.isSupported()) {
var hls = new Hls();
hls.loadSource(videoSrc);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function() {
video.play();
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = videoSrc;
video.addEventListener('loadedmetadata', function() {
video.play();
});
}
</script>
</body>
</html>な

なお、cdnを使っていますので、ブラウザー側はインターネットアクセスできる前提です。(サーバー(Rsapberry Pi)側はGoProのwifiにつかまっている)

スナップショット案

ふたつの方法でライブストリームを見てみました。しかし現状把握をすることが目的です。遅延が数十秒というのは実用になりません。一般的なライブストリームは予想以上に大きな遅延があることがわかりました。GoProをスマホから操るQUIKはUDPデータを直接描画しています。ブラウザー経由などでは間に合わないのですね。またffmpegでGoProプレビューストリームをデコードしているとエラーメッセージが大量に出ます。

リアルタイムプレビュー動画じゃあきらめて、オンラインで写真を取ってすぐに確認できるスナップショット機能を作りました。これならば以前のGoProに写真を取らせる方法の延長のようなものです。
Raspberry PiにLighttpd Webサーバーを用意します。ユーザーは特定のページをブラウザーから参照し、そこのボタンを押すと現在のGoproの写真を見ることができるというシンプルなものです。これだと5秒くらいの遅延です。かつ回線をそれほど消費しないので安定します。

Lighttpdをインストールします。

sudo apt install lighttpd

PythonをCGIで使いたいので次のコマンドでCGIを使用可能とします。

sudo lighttpd-enable-mod cgi

好みは分かれると思いますが、ドキュメントルートは自分のローカルにします。理由は「自分しか使わないから」
/etc/lighttpdlighttpd.conf にserver.document-root=とありますから、自分のフォルダー内にWeb用のフォルダーを作って指定します。(以降wwwとする) 確認のためのindex.htmlなんて「I’m OK」とでも書いておけばOK./home/自分のフォルダー /home/自分のフォルダー/www/ については読み取り可能にしておきます。

cgiでpythonを動かすためには設定ファイル /etc/lighttpd/conf-available/10-cgi.conf を編集します。

server.modules += ( "mod_cgi" )
$HTTP["url"] =~ "^/cgi-bin/"{
        cgi.assign = ( ".py" => "/usr/bin/python" )
}

pythonプログラムはcgi-binフォルダ以下に置くことになります。

submit(HTML内では「リフレッシュ」)ボタンを押されたら、pythonプログラムを起動し、写真ができれば画面再描画をします。
意外に面倒でしたが、HTMLは平凡に次のとおり。CSSはSLIMにおまかせ。なくてもいいと思います。

<!DOCTYPE html>
<html lang=”ja”>
<head>
<metacharset=”utf-8″/>
<metahttp-equiv=”Pragma” content=”no-cache”>
<metahttp-equiv=”Cache-Control” content=”no-cache”>
<meta name=”viewport” content=”width=device-width, initial-scale=1″/>
<link rel=”stylesheet” href=”//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic”>
<link rel=”stylesheet” href=”//cdn.rawgit.com/necolas/normalize.css/master/normalize.css”>
<link rel=”stylesheet”  href=”//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css”>
<style>
body{
padding:0;
margin:2px5px6px;
}
</style>
<title>GoPro Snap shot</title>
</head>
<body>
<h1>Gopro Snap Shot</h1>
<img src=”./snapshot.jpg” width=”500″>
<form action=”./cgi-bin/preview.py”>
<input type=”hidden” id=”currentURL” name=”currentURL” value=”dynamic”/>
<input type=”submit”value=”リフレッシュ”>
</form>
</body>
<script>
let passv = document.getElementById(“currentURL”)
passv.value = location.host + location.pathname
</script>
</html>

このHTMLで工夫しているところはふたつあります。
色をつけたところはイメージファイルを書き換えたら、ページのリロードすればいいだけなので、以前のイメージをもたないようにキャッシュをバイパスしています。
もうひとつ最後にあるJavascriptで現在のページのドメイン名、ページ名を入手してformのhiddenパラメーターを書き換えています。前のページに戻ってリロードするという方法はいろいろあると思いますが、この方法をとりました。

次がPythonで書いたCGIです。

#!/usr/bin/python3

import requests
import time
import json
import threading
import logging as log
from logging import getLogger
import cgi
import cgitb
import os

cgitb.enable()

logformat = "%(asctime)s %(levelname)s %(name)s :%(message)s"
log.basicConfig(filename="/home/tsukasa/www/cgi-bin/cgi.log", 
                format=logformat, level=log.DEBUG)

GOPRO_BASE_URL = "http://10.5.5.9:8080"
WRITE_PATH = "/home/tsukasa/www/snapshot.jpg"

def init():    
    response = requests.get(GOPRO_BASE_URL + 
                            "/gopro/camera/presets/set_group?id=1001",
                            timeout=10)
    time.sleep(1)
    response.raise_for_status()

    log.info("GoPro in picture mode.")

def send_shutter():
    log.info("Shutter Requested.")
    response = requests.get(GOPRO_BASE_URL + 
                            "/gopro/camera/shutter/start",
                            timeout=10)
    time.sleep(1)
    response.raise_for_status()

    response = requests.get(GOPRO_BASE_URL +
                            "/gopro/camera/shutter/stop",
                            timeout=10)
    time.sleep(1)
    response.raise_for_status()

def get_filename():

    log.info("get picture filename.")

    response = requests.get(GOPRO_BASE_URL + 
                            "/gopro/media/last_captured",
                            timeout=10)
    time.sleep(1)
    response.raise_for_status()

    path = json.loads(response.text)
    
    return path

def get_picture(path):
    time.sleep(1)
    filename = path["file"]
    foldername = path["folder"]
    url = GOPRO_BASE_URL + f"/videos/DCIM/{foldername}/{filename}"

    log.info("Download picture.")
    response = requests.get(url, stream=True, timeout=10)
    time.sleep(2)
    response.raise_for_status()
    
    
    with open(WRITE_PATH, "wb") as f:
        log.info(f"Receiving stream {filename} to {WRITE_PATH}")
        for chunk in response.iter_content(chunk_size=1024):
            f.write(chunk)
    time.sleep(1)
#------------

init()
send_shutter()
path = get_filename()
get_picture(path)

form = cgi.FieldStorage()
calledURL = form.getvalue('currentURL')
log.info(f"Called URL= {calledURL}")

print(f"Location: http://{calledURL}\n\n")
exit()

たいしたことしていないのですが、長くなってしまいました。
やってることは

  1. GoProをカメラモードにする
  2. シャッターを切る
  3. (GoPro12以降ですが)最後に撮った写真のファイル名を入手
  4. そのファイル名を使ってイメージをダウンロードする
  5. JavascriptからForm変数の値として渡されたURLページをリロードする

sleepがあちこち入っていますが、GoProが動作して返答したからといって次の処理を受け付けるとは限らず、503エラーをしばしば起こします。ですから時間調整です。(あまり好きじゃない)

PythonでCGIを初めて書きました。cgitbは必須ですね。これないとエラーの理由がわからん。
実行環境がやわなのが気になります。
GoProのWIFIアクセスポイントに接続していることと、スマホ、PCなどのブラウザーがLTEやイーサーなどで接続されていることが前提となります。(CSSがくずれても構わないならば、インターネット接続は必要ないです)

とりあえずこれで海行ってきます。

タイトルとURLをコピーしました