flvビデオをオンデマンド再生 ビデオソースをダウンロードされないようにする

flvビデオを簡単にオンデマンド再生する方法と、簡単にはビデオソースをダウンロードされない方法をまとめました。

FLVは再生側の取り扱いが面倒

Flashの再生がだんだん難しくなっており、ChromeなどはFlash再生のためにそのサイトの許可を書かないといけない必要が出てきています。これは多くの方にビデオを見ていただきたいという時にかなり不利です。面倒くさい作業はしてくれません。

FLVビデオオンデマンドを簡単に再生

nginxにrtmp-moduleを利用してflvをrtmp化して映像配信していましたが、上記Flashのブラウザ上のセキュリティ対策にぶち当たるようになりました。flvビデオを簡単にオンデマンド映像にし、後はそのビデオソースもダウンロードできないようにできないかと考え、下記の方法を実現しました。

まず、最近はHTML5のvideoタグで簡単にストリーミング再生できるようになっていますが、HTML5のvideoタグではflvファイルは再生できません。

ストリーミング再生は、ファイルをダウンロードしながら逐次再生していきます。一昔前までは映像をダウンロードしてから再生でした。HTML5のvideoタグを利用すると簡単にストリーミング再生が実現できます。ただ、そのままだと映像ソースをダウンロードされてしまうデメリット、セキュリティ上の問題もあります。また、HTML5のvideoタグではflvファイルはストリーミング再生できません。

wikiより

Flash Video(フラッシュ ビデオ)は、主にFlash Player 6以降を利用してインターネット上で動画を配信するために利用されるコンテナ型のファイルフォーマットで、元はマクロメディアが開発していたものを、アドビシステムズが会社ごと買収した。Flash VideoはSWFファイルの内部に埋め込まれる場合もある。

最近は、このflash形式での映像配信は、各ブラウザで制限されるようになりました。再生できない事もあるし、配信元の許可をブラウザに書かないと再生できないなど、使い勝手が非常に面倒になりました。

ただ、私達の環境では、nginx + rtmp-moduleを利用して映像配信を行っております。ライブ配信時、デフォルトで録画できる形式がflvです。一旦mp4などに変換をかけてしまえば難なくvideoタグで配信できるわけですが、その手間を惜しみ、flvをそのままオンデマンド配信する方法を実現しました。

web配置するflv.jsプレーヤは、Flashなしで純粋なJavaScriptで書かれたHTML5 Flash Video(FLV)プレーヤーです。このプレーヤーは、FLVファイルストリームをMP4セグメントに変換し、mp4セグメントをHTML5要素に送る事によって動作します。

特徴
H.264 + AAC / MP3コーデック再生によるFLVコンテナ
マルチパート分割ビデオ再生
HTTP FLV低遅延ライブストリーム再生
FLV over WebSocketライブストリーム再生
Chrome、FireFox、Safari 10、IE11、Edgeと互換性があります
非常にオーバーヘッドが少なく、ハードウェアはブラウザで高速化されます!

Screenshot of github.com

実際に設定してみます。flv.jsプレーヤーのダウンロード、コンパイルインストールし、webプレーヤーとして配置、htmlからsrcの指定、リファラで映像ソースのダウンロード禁止までを手順としています。

git clone https://github.com/Bilibili/flv.js.git

cd flv.js
npm i
npm run build

参考 npmの実行環境がない場合

# apt install curl
# curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
# apt-get install -y nodejs

videojsプレーヤーとは関係ないのですが、フォルダを作るのが面倒なので、videojsフォルダの中に、上記手順でできた flv.min.jsファイルを突っ込みます。

flv.js/dist# cp flv.min.js /usr/local/nginx/html/ondemand/videojs/

単純なプレーヤーの配置を考えてみます。

<!DOCTYPE html>
<html>

<head>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
    <title>VOD</title>
</head>

<!-- 右クリック禁止 -->
<body oncontextmenu="return false;">

<script src="./videojs/flv.min.js"></script>
<!-- ホバー時コントロールを表示する -->
<video id="videoElement" controls autoplay></video>
<script>
    if (flvjs.isSupported()) {
        var videoElement = document.getElementById('videoElement');
        var flvPlayer = flvjs.createPlayer({
            type: 'flv',
            url: 'http://cloudn.hanako.jp/movies/doga.flv'
        });
        flvPlayer.attachMediaElement(videoElement);
        flvPlayer.load();
        flvPlayer.play();
    }
</script>

</body>

</html>

パスワードを知っている人しか映像を見る事はできないようにします。

        location /movies/ {
                auth_basic "Eizo Ha Eizo";
                auth_basic_user_file "/usr/local/nginx/passwd/.htpasswd";
        }

自分サイトがアクセス元になっているアクセスからしか映像を再生させない。(直リンクを避ける意味合い)これにより、簡単に映像をダウンロードできなくなります。

        location ~ ^/movies/ {
             valid_referers server_names cloudn.hanako.jp;
             if ($invalid_referer) { return 403; }
        }

恐らく、リファラは偽証可能で破る方法があるのではないかと思います。かなり詳しい人にアタックされたらダウンロードできてしまうだろうけど、普通に映像を見に来ている人だったらそこまでしないだろうという発想です。

参考

http://bilibili.github.io/flv.js/demo/
 
<!DOCTYPE html>
<html>

<head>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
    <title>flv.js demo</title>
    <link rel="stylesheet" type="text/css" href="demo.css" />
</head>

<body>
    
    <div class="mainContainer">
        <div>
            <div id="streamURL">
                <div class="url-input">
                    <label for="sURL">Stream URL:</label>
                    <input id="sURL" type="text" value="http://127.0.0.1/flv/7182741-1.flv" />
                    <button onclick="switch_mds()">Switch to MediaDataSource</button>
                </div>
                <div class="options">
                    <input type="checkbox" id="isLive" onchange="saveSettings()" />
                    <label for="isLive">isLive</label>
                    <input type="checkbox" id="withCredentials" onchange="saveSettings()" />
                    <label for="withCredentials">withCredentials</label>
                    <input type="checkbox" id="hasAudio" onchange="saveSettings()" checked />
                    <label for="hasAudio">hasAudio</label>
                    <input type="checkbox" id="hasVideo" onchange="saveSettings()" checked />
                    <label for="hasVideo">hasVideo</label>
                </div>
            </div>
            <div id="mediaSourceURL" class="hidden">
                <div class="url-input">
                    <label for="msURL">MediaDataSource JsonURL:</label>
                    <input id="msURL" type="text" value="http://127.0.0.1/flv/7182741.json" />
                    <button onclick="switch_url()">Switch to URL</button>
                </div>
            </div>
        </div>
        <div class="video-container">
            <div>
                <video name="videoElement" class="centeredVideo" controls autoplay>
                    Your browser is too old which doesn't support HTML5 video.
                </video>
            </div>
        </div>
        <div class="controls">
            <button onclick="flv_load()">Load</button>
            <button onclick="flv_start()">Start</button>
            <button onclick="flv_pause()">Pause</button>
            <button onclick="flv_destroy()">Destroy</button>
            <input style="width:100px" type="text" name="seekpoint"/>
            <button onclick="flv_seekto()">SeekTo</button>
        </div>
        <textarea name="logcatbox" class="logcatBox" rows="10" readonly></textarea>
    </div>

    <script src="../dist/flv.js"></script>
    
    <script>
        var checkBoxFields = ['isLive', 'withCredentials', 'hasAudio', 'hasVideo'];
        var streamURL, mediaSourceURL;

        function flv_load() {
            console.log('isSupported: ' + flvjs.isSupported());
            if (mediaSourceURL.className === '') {
                var url = document.getElementById('msURL').value;
    
                var xhr = new XMLHttpRequest();
                xhr.open('GET', url, true);
                xhr.onload = function (e) {
                    var mediaDataSource = JSON.parse(xhr.response);
                    flv_load_mds(mediaDataSource);
                }
                xhr.send();
            } else {
                var i;
                var mediaDataSource = {
                    type: 'flv'
                };
                for (i = 0; i < checkBoxFields.length; i++) {
                    var field = checkBoxFields[i];
                    /** @type {HTMLInputElement} */
                    var checkbox = document.getElementById(field);
                    mediaDataSource[field] = checkbox.checked;
                }
                mediaDataSource['url'] = document.getElementById('sURL').value;
                console.log('MediaDataSource', mediaDataSource);
                flv_load_mds(mediaDataSource);
            }
        }

        function flv_load_mds(mediaDataSource) {
            var element = document.getElementsByName('videoElement')[0];
            if (typeof player !== "undefined") {
                if (player != null) {
                    player.unload();
                    player.detachMediaElement();
                    player.destroy();
                    player = null;
                }
            }
            player = flvjs.createPlayer(mediaDataSource, {
                enableWorker: false,
                lazyLoadMaxDuration: 3 * 60,
                seekType: 'range',
            });
            player.attachMediaElement(element);
            player.load();
        }

        function flv_start() {
            player.play();
        }

        function flv_pause() {
            player.pause();
        }

        function flv_destroy() {
            player.pause();
            player.unload();
            player.detachMediaElement();
            player.destroy();
            player = null;
        }

        function flv_seekto() {
            var input = document.getElementsByName('seekpoint')[0];
            player.currentTime = parseFloat(input.value);
        }

        function switch_url() {
            streamURL.className = '';
            mediaSourceURL.className = 'hidden';
            saveSettings();
        }

        function switch_mds() {
            streamURL.className = 'hidden';
            mediaSourceURL.className = '';
            saveSettings();
        }

        function ls_get(key, def) {
            try {
                var ret = localStorage.getItem('flvjs_demo.' + key);
                if (ret === null) {
                    ret = def;
                }
                return ret;
            } catch (e) {}
            return def;
        }

        function ls_set(key, value) {
            try {
                localStorage.setItem('flvjs_demo.' + key, value);
            } catch (e) {}
        }

        function saveSettings() {
            if (mediaSourceURL.className === '') {
                ls_set('inputMode', 'MediaDataSource');
            } else {
                ls_set('inputMode', 'StreamURL');
            }
            var i;
            for (i = 0; i < checkBoxFields.length; i++) {
                var field = checkBoxFields[i];
                /** @type {HTMLInputElement} */
                var checkbox = document.getElementById(field);
                ls_set(field, checkbox.checked ? '1' : '0');
            }
            var msURL = document.getElementById('msURL');
            var sURL = document.getElementById('sURL');
            ls_set('msURL', msURL.value);
            ls_set('sURL', sURL.value);
            console.log('save');
        }

        function loadSettings() {
            var i;
            for (i = 0; i < checkBoxFields.length; i++) {
                var field = checkBoxFields[i];
                /** @type {HTMLInputElement} */
                var checkbox = document.getElementById(field);
                var c = ls_get(field, checkbox.checked ? '1' : '0');
                checkbox.checked = c === '1' ? true : false;
            }

            var msURL = document.getElementById('msURL');
            var sURL = document.getElementById('sURL');
            msURL.value = ls_get('msURL', msURL.value);
            sURL.value = ls_get('sURL', sURL.value);
            if (ls_get('inputMode', 'StreamURL') === 'StreamURL') {
                switch_url();
            } else {
                switch_mds();
            }
        }

        function showVersion() {
            var version = flvjs.version;
            document.title = document.title + " (v" + version + ")";
        }

        var logcatbox = document.getElementsByName('logcatbox')[0];
        flvjs.LoggingControl.addLogListener(function(type, str) {
            logcatbox.value = logcatbox.value + str + '\n';
            logcatbox.scrollTop = logcatbox.scrollHeight;
        });

        document.addEventListener('DOMContentLoaded', function () {
            streamURL = document.getElementById('streamURL');
            mediaSourceURL = document.getElementById('mediaSourceURL');
            loadSettings();
            showVersion();
            flv_load();
        });
    </script>
    
</body>

</html>