3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PCにある動画ファイルを、ブラウザを通じてスマホやタブレットで見るために作った簡易HTTPサーバーの開発記録 2

Last updated at Posted at 2020-05-22

これは前の記事の続きです
https://qiita.com/amate/items/d92fe184bed98076e8e7


std::wregex rx(LR"(\?path=\/root\/(.*)");
std::wsmatch result;
if (std::regex_match(queryData, result, rx)) {
    auto searchFolderPath = result[1].str();
    FileTreeAPI(searchFolderPath, yield, s);
}

FileTreeAPIは、GETリクエストで
http://localhost:4789/filefreeAPI?path=<ファイルリストを取得するフォルダのパス>
のようなリクエストが来た場合、フォルダ内のファイルリストをjson形式で返すAPIとして実装します
そのため、<ファイルリストを取得するフォルダのパス>の部分を正規表現で抜きだして、関数に渡すようにします

void FileTreeAPI(const std::wstring& searchFolderPath, asio::yield_context& yield, tcp::socket& s)
{
    struct FileItem {
        std::wstring name;
        bool	isFolder;
        std::wstring fileSize;

        FileItem(const std::wstring& name, bool isFolder, const std::wstring& fileSize)
            : name(name), isFolder(isFolder), fileSize(fileSize) {}
    };

    std::list<FileItem> fileList;
    const fs::path rootFolder = LR"<ルートフォルダ>";
    const fs::path searchFolder = rootFolder / searchFolderPath;
    if (!fs::is_directory(searchFolder)) {
        json jsonFolder;
        jsonFolder["Status"] = "failed";
        jsonFolder["Message"] = "searchFolder not exists";
        SendJSON200OK(jsonFolder.dump(), yield, s);
        return;
    }

    for (auto p : fs::directory_iterator(searchFolder)) {
        bool isFolder = fs::is_directory(p);
        if (isMediaFile(p) || isFolder) {
            std::wstring strfileSize;
            if (!isFolder) {
                auto fileSize = fs::file_size(p.path());
                WCHAR tempBuffer[64] = L"";
                ::StrFormatByteSizeW(fileSize, tempBuffer, 64);
                strfileSize = tempBuffer;
            }
            fileList.emplace_back(p.path().filename().wstring(), isFolder, strfileSize);
        }
    }
    
    std::sort(fileList.begin(), fileList.end(), 
        [](const FileItem& n1, const FileItem& n2) {
            if (n1.isFolder == n2.isFolder) {
                return ::StrCmpLogicalW(n1.name.c_str(), n2.name.c_str()) < 0;
            } else {
                return n1.isFolder;
            }
    });

    json jsonFileList = json::array();
    for (auto fileItem : fileList) {
        jsonFileList.push_back({
            {"name", ConvertUTF8fromUTF16(fileItem.name)},
            {"isFolder", fileItem.isFolder},
            {"FileSize", ConvertUTF8fromUTF16(fileItem.fileSize)},
        });
    }
    json jsonFolder;
    jsonFolder["Status"] = "ok";
    jsonFolder["FileList"] = jsonFileList;
    SendJSON200OK(jsonFolder.dump(), yield, s);
}

FileTreeって名前がいまいちよくないな・・・
ファイルだけじゃなくてフォルダも返すし、木構造でもない
プログラミングでの名付けは重要なので、気を付けたい

for (auto p : fs::directory_iterator(searchFolder))

fs は、boost::filesystem である
range base forで、directory_iteratorをこんな風に回せると知ったときは、目から鱗が落ちるほど関心した
単純に beginとend を実装してあるだけなのだが

auto fileSize = fs::file_size(p.path());
WCHAR tempBuffer[64] = L"";
::StrFormatByteSizeW(fileSize, tempBuffer, 64);

StrFormatByteSizeW は fileSizeの数値によって

fileSize tempBuffer
532 532 bytes
23506 22.9 KB
2400016 2.28 MB
2400000000 2.23 GB

のようにテキスト形式に変換してくれる便利なAPIだ
こうした予め知っていた便利なAPIを実際に使えると、プログラミングを楽しく感じる

json jsonFolder;
jsonFolder["Status"] = "ok";
jsonFolder["FileList"] = jsonFileList;
SendJSON200OK(jsonFolder.dump(), yield, s);

jsonは、JSON for Modern C++というライブラリを使用している
C++でjsonを扱いたい場合はこれ一択と言ってもいいレベルで素晴らしいライブラリだ

void SendJSON200OK(const std::string& jsonData, asio::yield_context& yield, tcp::socket& s)
{
    std::stringstream	ss;
    ss << "HTTP/1.1 200 OK\r\n"
        << "Content-Type: " << "application/json" << "\r\n"
        << "Connection: close\r\n\r\n";
    std::string header = ss.str();
    boost::system::error_code ec;
    asio::async_write(s, asio::buffer(header), yield[ec]);
    if (ec) {
        ERROR_LOG << L"SendJSON200OK - async_write failed: " << UTF16fromShiftJIS(ec.message());
        return;
    }
    asio::async_write(s, asio::buffer(jsonData), yield[ec]);
    if (ec) {
        ERROR_LOG << L"SendJSON200OK - async_write failed: " << UTF16fromShiftJIS(ec.message());
        return;
    }

    INFO_LOG << L"\tsend: " << jsonData.size() << L" bytes";
    s.close();
}

今思ったら、SendFile200OKと機能がかぶっててあんまりよろしくないかもしれない・・・
SendFile200OKはファイルのパスを取るのに対して、SendJSON200OKはデータ自身を引数に取るという違いはあるが


APIの実装が終わったので、実際にアクセスしてみて確かめてみる
http://localhost:4789/filetreeAPI?path=/root/

ss9.jpg

ちゃんとデータが送られてくることが確認できた
firefoxはjsonデータを整形して表示してくれるので便利


今度は、htmlの方を編集する

root.html
<table class="table" id="FileListTable">

取得したファイルリストを挿入していくテーブルを見つけやすいように、予めidを書いておく

root.html
    <script src="/filetree.js"></script>
</body>

htmlフォルダ内に、filetree.jsを作成して
スクリプトを読み込むために</body>の手前に <script>タグを書いておく

前はroot.htmlに直接javascriptを書いていたが、VSCodeでは.jsファイルにしかブレイクポイントを設定できないので、スクリプト用のファイルを作成しておく
これに気づかず、どうしてブレイクポイント設定できないんだろうと延々試行錯誤してました・・・1

filetree.js
// ファイルリスト読み込み
let queryTree = {
    path: decodeURI(location.pathname)
};

$.getJSON("/filetreeAPI", queryTree)
.done(function(json){
    if (json.Status != 'ok') {
        return;
    }

    let FileList = json.FileList;
    for (let i = 0; i < json.FileList.length; ++i) {
        let item = json.FileList[i];
        //console.log(item);
        let tdIcon = $('<td>');
        let anchorURL = "";
        if (item.isFolder) {
            tdIcon.append('<i class="fa fa-folder  fa-fw" aria-hidden="true"></i>');
            anchorURL = location.pathname + item.name + '/';
        } else {
            tdIcon.append('<i class="fa fa-film  fa-fw" aria-hidden="true"></i>');
            anchorURL = "/play" + location.pathname.substr(5) + item.name;
        }
        anchorURL = anchorURL.replace(/#/g, "<hash>") + location.search;
        let nameAnchor = $('<a>', {
            href: anchorURL
        }).text(item.name);

        let tdName = $('<td>').append(nameAnchor);
        if (!item.isFolder) {
            tdName.append('<span class="float-right">' + item.FileSize + '</span>');
        }
        let tr = $('<tr>').append(tdIcon).append(tdName);
        $('#FileListTable').append(tr);
    }
    //console.log(json);
})
.fail(function(jqXHR, textStatus, errorThrown) {
    console.error("getJSON fail: " + textStatus);
});

let queryTree = {
    path: decodeURI(location.pathname)
};

$.getJSON("/filetreeAPI", queryTree)

jQueryは、$.getJSONという引数に渡されたURLにアクセスして、jsonデータを取得してきてくれる関数があるので、それを利用する

FileTreeAPIは、pathに<ファイルリストを取得するフォルダのパス>を渡さなければならないが、フォルダのパスは単純にURLのパス部分から取得すればよい
例えば、http://localhost:4789/root/サブフォルダ/ をブラウザで開いた場合、location.pathnameは"/root/サブフォルダ/"となるので、それを渡してあげればいい
上記の仕様にすることによって、サーバー側の実装も簡単になるメリットがある
/root/以下へのアクセスは 毎回root.htmlを返すだけでよくなるのだから

HttpServer.cpp
// ====================================================
// /root/
if (path.substr(0, 6) == "/root/") {
    auto templatePath = GetExeDirectory() / L"html" / L"root.html";
    SendFile200OK(templatePath, yield, s);
    break;
} else if (path.substr(0, 12) == "/filetreeAPI") {

サーバー側も実装完了
たった4行追加するだけで済んだ

for (let i = 0; i < json.FileList.length; ++i)

for文以下はテーブルにファイルリストを追加していく処理である

if (item.isFolder) {
    tdIcon.append('<i class="fa fa-folder  fa-fw" aria-hidden="true"></i>');
    anchorURL = location.pathname + item.name + '/';
} else {
    tdIcon.append('<i class="fa fa-film  fa-fw" aria-hidden="true"></i>');
    anchorURL = "/play" + location.pathname.substr(5) + item.name;
}

フォルダの場合はリンク先が、"/root/フォルダ名/"となるように設定し
動画ファイルの場合はリンク先を、"/play/動画ファイル名"となるように設定してある

anchorURL = anchorURL.replace(/#/g, "<hash>") + location.search;

この処理は、リンク先のURLに"#"が存在すると"#"以降の文字列が送信されないという仕様があるので、それを回避するために置換している
"<"や">"はファイル名として使用できないので、置換してもファイル名が被ることがない
サーバー側で"<hash>"を"#"に置換してあげれば、実際のファイル名を得られる寸法だ


実装が完了したので、サーバー側もコンパイルして http://localhost:4789/root/ にアクセスすると、$.getJSONなんて定義されてねーぞ!と怒られてしまった・・・

root.html
<script src="https://code.jquery.com/jquery-3.5.1.min.js" crossorigin="anonymous"></script>

jQuery-slimにはajaxのサポートが省かれているので、フル機能入ってるURLに書き換える

root.html
<!doctype html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">

    <title>Hello, world!</title>
  </head>
  <body>
    <div class="container">

    <table class="table" id="FileListTable">
        <thead>
          <tr>
            <th scope="col">#</th>
            <th scope="col">Name</th>
          </tr>
        </thead>
        <tbody>
        </tbody>
      </table>

    </div>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>

    <script src="/filetree.js"></script>
  </body>
</html>

その他テーブルを調節して、アイコン用のcssを追加して完成!
http://localhost:4789/root/ にアクセスすると、ファイルリストが表示された

ss10.jpg

フォルダ名をクリックするとフォルダが移動できる!
すごいアプリっぽい!
実際に自分が作ったプログラムがそれっぽく動くと感動する


ファイルリスト画面は完成したので、次は肝心の再生画面を作っていく
が、その前にサーバー側でHLS用のインデックスファイルとセグメントファイルを用意するAPIを作る

} else if (path.substr(0, 14) == "/PrepareHLSAPI") {
    auto queryData = ConvertUTF16fromUTF8(URLDecode(path.substr(14)));
    std::wregex rx(LR"(\?path=\/play\/(.*))");
    std::wsmatch result;
    if (std::regex_match(queryData, result, rx)) {
        auto mediaPath = result[1].str();
        boost::replace_all(mediaPath, L"<hash>", L"#");
        PrepareHLSAPI(mediaPath, yield, s);
    }
    break;
} 

filetreeAPIの下に、PrepareHLSAPIを用意する
filetreeAPIと同じように、正規表現でパスを抜き出してPrepareHLSAPI関数に渡すだけだ

void PrepareHLSAPI(const std::wstring& mediaPath, asio::yield_context& yield, tcp::socket& s)
{
    const fs::path rootFolder = LR"(<ルートフォルダ>)";
    const fs::path actualMediaPath = (rootFolder / mediaPath).make_preferred();
    if (!fs::is_regular_file(actualMediaPath)) {
        json jsonResponse;
        jsonResponse["Status"] = "failed";
        jsonResponse["Message"] = "file not found";
        SendJSON200OK(jsonResponse.dump(), yield, s);
        return;
    }

    // 作業フォルダ作成
    const fs::path segmentFolderPath = BuildWokringFolderPath(actualMediaPath);
    if (!fs::is_directory(segmentFolderPath)) {
        if (!fs::create_directories(segmentFolderPath)) {
            THROWEXCEPTION(L"create_directories(segmentFolderPath) failed");
        }
    }

    const fs::path playListPath = segmentFolderPath / L"a.m3u8";
    if (!fs::exists(playListPath)) {	// プレイリストファイルが存在しなければセグメントファイルはまだ未生成
        fs::path enginePath = LR"(C:\#app\tv\#enc\ffmpeg-20200504-5767a2e-win64-static\bin\ffmpeg.exe)";
        if (!fs::exists(enginePath)) {
            ERROR_LOG << L"enginePath に実行ファイルが存在しません: " << enginePath.wstring();

            json jsonResponse;
            jsonResponse["Status"] = "failed";
            jsonResponse["Message"] = "enginePath not exists";
            SendJSON200OK(jsonResponse.dump(), yield, s);
            return;
        } else {
            std::wstring commandLine = BuildVCEngineCommandLine(actualMediaPath, segmentFolderPath);
            std::thread([=]() {
                // エンコーダー起動
                StartProcess(enginePath, commandLine);
            }).detach();

            enum {
                kMaxRetryCount = 5
            };
            int retryCount = 0;
            while (!fs::exists(playListPath)) {
                ++retryCount;
                if (kMaxRetryCount < retryCount) {
                    ERROR_LOG << L"エンコードに失敗? 'a.m3u8' が生成されていません";

                    json jsonResponse;
                    jsonResponse["Status"] = "failed";
                    jsonResponse["Message"] = "encode failed";
                    SendJSON200OK(jsonResponse.dump(), yield, s);
                    return;	// failed
                }
                ::Sleep(1000);
            }
        }
    }
    std::string playListURL = "/stream/";
    playListURL += ConvertUTF8fromUTF16(segmentFolderPath.filename().wstring()) + "/a.m3u8";
    json jsonResponse;
    jsonResponse["Status"] = "ok";
    jsonResponse["playListURL"] = playListURL;
    SendJSON200OK(jsonResponse.dump(), yield, s);
}
fs::path BuildWokringFolderPath(const fs::path& actualMediaPath)
{
    const auto canonicalPath = fs::path(actualMediaPath).make_preferred();
    std::hash<std::wstring> strhash;
    wchar_t workingFolderName[128] = L"";	// 最大でも "32 + 1 + 16 = 49" なので大丈夫
    swprintf_s(workingFolderName, L"%zx", strhash(canonicalPath.wstring()));

    fs::path segmentFolderPath = GetExeDirectory() / L"html" / L"stream" / workingFolderName;
    return segmentFolderPath.make_preferred();
}

始めに、インデックスファイルやセグメントファイルを置いておく作業フォルダを "/html/stream/"フォルダに作成する
フォルダ名は、動画ファイルのフルパスから得たハッシュ値にする
多分ハッシュ値が被ることは無い・・・と願いたい

std::wstring BuildVCEngineCommandLine(const boost::filesystem::path& mediaPath,
    const boost::filesystem::path& segmentFolderPath)
{
    const fs::path actualMediaPath = boost::filesystem::path(mediaPath).make_preferred();

    std::wstring commandLine = LR"( -i "<input>" -map 0:v -map 0:a  -c:v libx264 -preset veryfast -crf 23  -keyint_min 30 -g 30   -c:a aac -b:a 192k  -f hls -hls_time 5 -hls_list_size 0 -hls_segment_filename "<segmentFolder>\segment_%08d.ts" "<segmentFolder>\a.m3u8")";

    boost::replace_all(commandLine, L"<input>", actualMediaPath.wstring().c_str());

    std::string ext = boost::to_lower_copy(actualMediaPath.extension().string());

    boost::replace_all(commandLine, L"<segmentFolder>",
        boost::filesystem::path(segmentFolderPath).make_preferred().wstring().c_str());

    return commandLine;
}

動画ファイルと作業ファイルのパスから、ffmpegに渡すコマンドライン引数を作成

/// プロセスを起動し、終了まで待つ
DWORD	StartProcess(const fs::path& exePath, const std::wstring& commandLine)
{
	INFO_LOG << L"StartProcess\n" << L"\"" << exePath.wstring() << L"\" " << commandLine;

	STARTUPINFO startUpInfo = { sizeof(STARTUPINFO) };
	startUpInfo.dwFlags = STARTF_USESHOWWINDOW;
#ifdef _DEBUG
	startUpInfo.wShowWindow = SW_NORMAL;
#else
	startUpInfo.wShowWindow = SW_HIDE;//SW_NORMAL;
#endif
	PROCESS_INFORMATION processInfo = {};
	SECURITY_ATTRIBUTES securityAttributes = { sizeof(SECURITY_ATTRIBUTES) };
	BOOL bRet = ::CreateProcess(exePath.native().c_str(), (LPWSTR)commandLine.data(),
		nullptr, nullptr, FALSE, CREATE_NEW_CONSOLE, nullptr, nullptr, &startUpInfo, &processInfo);
	if (bRet == 0) {
		ATLASSERT(FALSE);
		THROWEXCEPTION(L"StartProcess");
	}
	//ATLASSERT(bRet);
	::WaitForSingleObject(processInfo.hProcess, INFINITE);
	DWORD dwExitCode = 0;
	GetExitCodeProcess(processInfo.hProcess, &dwExitCode);

	::CloseHandle(processInfo.hThread);
	::CloseHandle(processInfo.hProcess);

	return dwExitCode;
}

ffmpegにコマンドライン引数を渡して、インデックスファイルとセグメントファイルの生成開始

std::string playListURL = "/stream/";
playListURL += ConvertUTF8fromUTF16(segmentFolderPath.filename().wstring()) + "/a.m3u8";
json jsonResponse;
jsonResponse["Status"] = "ok";
jsonResponse["playListURL"] = playListURL;
SendJSON200OK(jsonResponse.dump(), yield, s);

生成されたインデックスファイル(a.m3u8)のパスを"playListURL"に設定し、json形式で返すAPIを実装完了


サーバー側のAPIは実装したので、今度はhtml側を作っていく

play.html
<!doctype html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">

        <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.4.1.min.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>

    <link rel="icon" type="image/png" href="/favicon.png"/>
    <title>VideoConvertStreamServer - Play</title>

    <style>
      .video-container {
        position: relative;
      }
      #video {
        background-color: black;
      }
      .play-button {
        position: absolute;
        top: 50%;
        left: 50%;

        margin-top: -54px;
        margin-left: -48px;

        border-radius: 50%;
        background-color: black;
        border-color: black;
    }
    </style>
  </head>
  <body>

<div class="container">
    <hr>  
      <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
      <!-- Or if you want a more recent canary version -->
      <!-- <script src="https://cdn.jsdelivr.net/npm/hls.js@canary"></script> -->
    <div class="video-container" id="video-container">
      <video id="video" controls width="100%"></video>

      <!-- Play Button -->
      <button class="btn btn-dark play-button" id="PlayButton"><i class="fa fa-play-circle fa-5x" aria-hidden="true"></i></button>

      <!-- <div id="SwipeSeekArea"></div> -->
    </div>
    <hr>

</div> <!-- class="container" -->
<script src="/play.js"></script>

</body>
</html>

HLS形式のライブストリーミングは、Edgeかsafariでしか標準で再生できないので、その他のブラウザでも再生できるようにする hls.js というライブラリを利用します

<!-- Play Button -->
<button class="btn btn-dark play-button" id="PlayButton"><i class="fa fa-play-circle fa-5x" aria-hidden="true"></i></button>

再生ボタンを追加
このボタンが押された時に、PrepareHLSAPIを呼び出すように実装していきます

<style>
.play-button 
{
    position: absolute;
    top: 50%;
    left: 50%;
}
</style>

スタイルシートで、再生ボタンをプレイヤーの真ん中に置こうとしたが

ss11.jpg

なぜかズレて表示されてしまう現象に悩んだが

.video-container {
position: relative;
}

ググった結果、親要素の positionをrelativeにしてやればいいとの話だったので、そのようにcssを変更したら無事解決

ss12.jpg

html部分は出来たので、次はplay.jsを書いていく

play.js

var QueryPrepareHLS = function()
{
    let filePath = decodeURI(location.pathname).replace(/<hash>/g, "#");

    let queryPlay = {
        path: filePath
    };

    $.getJSON("/PrepareHLSAPI", queryPlay)
    .done(function(json){
        var video = document.getElementById('video');
        if (json.Status != "ok") {
            return ;
        }

        let playListURL = json.playListURL;
        console.log('playListURL: ' + playListURL);

        var videoSrc = playListURL;//'/stream/test.m3u8';
        if (Hls.isSupported()) {
            let hlsConfig = Hls.DefaultConfig;
            hlsConfig.startPosition = 0;

            var hls = new Hls(hlsConfig);
            hls.loadSource(videoSrc);
            hls.attachMedia(video);
            hls.on(Hls.Events.MANIFEST_PARSED, function() {
             video.play();
            });
        }
        // hls.js is not supported on platforms that do not have Media Source
        // Extensions (MSE) enabled.
        //
        // When the browser has built-in HLS support (check using `canPlayType`),
        // we can provide an HLS manifest (i.e. .m3u8 URL) directly to the video
        // element through the `src` property. This is using the built-in support
        // of the plain video element, without using hls.js.
        //
        // Note: it would be more normal to wait on the 'canplay' event below however
        // on Safari (where you are most likely to find built-in HLS support) the
        // video.src URL must be on the user-driven white-list before a 'canplay'
        // event will be emitted; the last video event that can be reliably
        // listened-for when the URL is not on the white-list is 'loadedmetadata'.
        else if (video.canPlayType('application/vnd.apple.mpegurl')) {
            video.src = videoSrc;
            video.addEventListener('loadedmetadata', function() {
            video.play();
            });
        }
        //console.log(json);
    })
    .fail(function(jqXHR, textStatus, errorThrown) {
        console.error("getJSON fail: " + textStatus);
    });

};

$('#PlayButton').on('click', function() {
    $('#PlayButton').hide();
    QueryPrepareHLS();
});

ほとんどhls.jsのサンプルのままだ

var videoSrc = playListURL;

videoSrcにplayListURLを突っ込むだけで簡単に再生してくれる
素晴らしい

filetreeAPIのときと同じく、ブラウザから/play/以下にアクセスされた場合は、play.htmlを返すようにサーバー側に実装を追加する

// ====================================================
// /play/
} else if (path.substr(0, 6) == "/play/") {
    auto templatePath = GetExeDirectory() / L"html" / L"play.html";
    SendFile200OK(templatePath, yield, s);
    break;
} else if (path.substr(0, 14) == "/PrepareHLSAPI") {

実装はすべて完了したので、サーバーをコンパイルしてブラウザから適当な動画を開いてみると・・・

ss13.jpg

無事動画が再生出来た!


完成

以上で、ファイルリストから動画を選択してブラウザで再生するところまで作り、一つのアプリケーションとして完成した

完成したとなれば、配布したいなーとなるが
配布するためには、事前に確認しておかなければならない事項がある
使用しているライブラリのライセンスの確認である

Server

html

上記が、使用ライブラリとライセンスの一覧である
Boost Software LicenseやMIT Licenseのような制限が緩いライセンスは、無保証であることと、著作権表記、それにライセンス文自体を配布物に含めることで、自分が作成したプログラムを配布することが許諾される

ffmpegはGPLとLGPLとのデュアルライセンスだが、映像のエンコードに利用しているx264ライブラリがGPLなので、自動的にGPLとなる
GPLは制限が厳しくあまり選択したくないが、実行ファイルをコマンドライン引数を用いてやり取りするぐらいなら自分のプログラムをGPLにしなくてもよいような気もする・・・が、あまり詳しくないので他の手を考える

openh264というBSDライセンスのh264エンコーダーを見つけた
このライブラリをffmpegに組み込めばLGPLにできるので、ありがたく利用させてもらう

ffmpegのビルドは大変そうなので、既にビルド済みの実行ファイルを公開してくれている方のを利用させてもらう
https://github.com/lembryo/ffmpeg

ライブラリの問題は片付いたが、今度はH.264自体のライセンスを確認しなければならない
https://www.netanote.com/2019/06/h-264%E3%81%AE%E3%83%A9%E3%82%A4%E3%82%BB%E3%83%B3%E3%82%B9%E6%96%99/

製品にエンコーダーとデコーダーが一つずつとして、暦年で年間100,000以内の製品であれば、ライセンス料金がかからない

10万ダウンロードなんてされようもないので問題解決

次はAACだ
https://www.via-corp.com/jp/ja/licensing/aac/faq.html

エンコーダーやデコーダーの販売時のみ、ライセンス料金をお支払いただきます。

販売するわけではないので大丈夫だろう

これでライセンスに関する問題はすべて解決したので、アプリケーションを配布できるぞ!


終わり

これでようやく、スマホやタブレットでごろ寝しながら録画した動画を見ることができるようになった

最近はブラウザで何でもできるようになっていてびっくりした
qiitaがウェブ技術の記事だらけなのも頷ける

一つのアイデアから実際に手を動かし、プログラムを作っていく
完成したプログラムで問題を解決できる
これがプログラミングの楽しいところであり、技術を持っているからこそできる問題解決の方法だ

皆もプログラミングをしよう!楽しいぞ!


  1. この記事で解決できました https://blog.janjan.net/2019/10/13/vsc-set-breakpoint-local-html-javascript/

3
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?