2018年9月30日日曜日

Vtuberの第一回 ライブ王決定戦 感想など

まぁ、うーんって感じでしたね

数字を取ったので軽くまとめ
縦軸の時間が22:30です。
※全員が生放送開始しないと取得できないコード書いてたので
22:02ぐらいからです

視聴者数
兎と種が競り合って逆転からの逆転ですね
豆は最後見捨てられて狸に既に移動してる感じ




視聴者数/開始時チャンネル登録者数(%)
新しいVtuberの方がアクティブは多いのかな?
種の人は96%もあります。ゲスト効果とかあるのでしょうが。すごいです。
急増はなに結果でしょうかね




チャンネル登録者数増加
アクティブな人を数百人増加ならメリットかな

高評価と低評価
高評価の右肩上がりな二人とフラットな二人
豆の低評価の急増と、インコの低評価の低さよ






勝者決定後(一部抜けあり)
なんで視聴者数一度減って増えているのだろうか?
ちなみに登録数増加1500人以上。

22:30時点の4人の視聴者数48450人。
決定後の最大視聴者数27401人
ってことはおおざっぱに21000人分が複窓か

2018年9月27日木曜日

プレミア動画と生放送の違い

プレミア動画というものを、ぽんぽこちゃんの動画で知ったのでAPIの挙動をどたばたしながらチェック
(収益化以上の状態必要っぽいけど、初めて見たのでここぞとばかりに確認)

全部確認できんかったけど、結果。


・検索のAPIの生放送でないので「&eventType=upcoming」や「&eventType=live」でヒットしない。
なので、最新の動画「&order=date」あたりで1番目に来るはずなのでみつけることもできる。
ゲリラとして使うことはないでしょから、放送前を見つけて動画IDをURLから直接とることもできる。
・プレミア動画は通常の動画違い[snippet] [liveBroadcastContent] => upcoming となっています。これを引っかけると自動検出できるかも。
※upcoming(放送前の状態)、放送中ならliveになる
・チャットは生放送と同じようにactiveLiveChatIdを取得できるのでいつものノリでチャットの取得可能
・視聴者数["liveStreamingDetails"]["concurrentViewers"]は、はっきり確認してないけどログを見ると無いかも。視聴者数はブラウザ上に表示できていたので他の方法で取得できるかも。

というわけで、API的にはちょっと違うのは検索ぐらいっぽいです。


ちなみにプレミア動画の流れ
 生放送のように、放送前からチャット可能
 時間になるとカウントダウンが2分程度始まる(必ずある?)
 その後、プレミア動画が始まる
 終了後も生放送のように終了する
 更新すると普通の動画に放送中のチャットが追加されたようになる

あっ、生放送通知については未確認です。

youtubeは、生放送でチャットのリプレイ追加などしているけどAPIは更新されてないみたい。
プレミア動画フラグとかありそうなんだけどなぁ。
※リプレイはソースからがんばって取得できるらしいけど。APIからじゃないのでちょっとアレ




2018年8月28日火曜日

画像から文字を認識してさらに翻訳!

youtubeや洋ゲー、漫画でちょっと出てくる外国語をぱっと翻訳できれば便利だと思いませんか?
気になった文を手打ちで写すのは面倒です。
これをgoogleのを画像認識を使って翻訳できるようにします。
若干手作業も入るので半自動って感じです。
APIと既存のソフトの組み合わせでこんなに便利になります!

準備として
①今回、WinShotというフリーのスクリーンショットを活用します。
保存先のフォルダの監視できればいいので、他のスクリーンショットソフトでもいいですがWinShotは優秀なのでお勧め。 矩形範囲指定かつPNGが保存できるようにしてください。
前回のノリでGoogle Play Consoleで「Google Cloud Translation API」を有効にします。
③さらに「Cloud Vision API」も有効します。

※②と③のサービスは有料です。洋ゲーで使いまくらなければ無料分で納まるかと思います。料金体系を理解して使用してください。便利なサービスなのでお金を払うのに値すると思う。
毎度のことですがAPIキーが他人の手にわたらないように!



以下、今回のコードの仕様
・「http://localhost/ocr_trs.php」をブラウザに入力して使用を開始します。
・ブラウザでコードのPHPを起動して、スクリーンショットを取ると自動で翻訳します。
・スクリーンショットで画像が保存されるたびにその画像を翻訳します。
・WinShotはショートカットキーで矩形範囲指定が行えるようにする。
そして読ませたい文字の部分を少し広めに囲って拡張子PNGで保存します。
コードはPNG対応になっています。文字がぼやっとするのが嫌なのでPNGにしています。
・認識した原文と翻訳後を表示する。
(画像認識結果がいまいちな場合は編集できるように原文をコピペできるように表示する)
・言語の指定をする必要が無いように言語検出を行う。 (その分、料金が発生することに注意)
・検出した言語→日本語の翻訳を行います。(日本語⇒日本語はエラーになってしまう)
・読みやすいようにピリオド以外の改行を無くした後、ピリオドで改行を行う


C:\xampp\htdocs直下に以下の2つのコードを保存してください。
1つ目
このコードはスクリーンショットソフトがPNGファイルを保存するフォルダを監視し続けます。
そして新規ファイルが保存されたらそのファイル名を返します。
どうもPHPではphpファイル直下のファイルしか画像表示できないようなので、「test.PNG」というファイル名を直下にコピーしています。
フォルダパスは自分の環境に合わせてください。

filechk.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
 set_time_limit(60*60);//タイムアウト時間を60分に変更
 
 //現在のフォルダ内のファイルを配列に入れる
 $basef = glob("C:\\Users\\main\\Documents\\tool\\WinShot\\Shot\\*.PNG");
 
 $flag = 1;
 while($flag)
 {
  //1 秒待つ
  usleep(1*1000000);
  $newf glob("C:\\Users\\main\\Documents\\tool\\WinShot\\Shot\\*.PNG");
 
  //ファイルの差分を取得
  $difff = array_diff($newf,$basef);
  foreach($difff as $file)
  {
      if(is_file($file))
      {
    usleep(0.1*1000000);//必要?
          $flag = 0;
          copy ($file, "C:\\xampp\\htdocs\\test.PNG");//表示用
          break;
      }
  }
 // $basef  = $newf;
 }
 echo $file;
?>



注釈として、
このコードは2つ目のコードから呼ばれます。
5行目で最初のフォルダの状況を保存し、12行目の最新のフォルダの状況を取得。
15行目で差分をとり、その差分があれば28行目でそのファイル名を返します。
※固定の名前でコピーしているのだからファイル名を返す必要はないのかも。。。
11行目で1秒毎に監視しています。頻度を早くするとレスポンスが少し良くなると思います。
20行目コピーを行うときに待ちなしで行うと失敗することがあるのでちょっとだけまっています
22行目表示用にコピーを行います。


2つ目
①は自分のAPIキー。
新規に画像が保存されたらそのファイルを画像認識のAPIに投げます。
次に画像認識結果のテキスト部分を翻訳のAPIに投げるという単純な構造です。

ocr_trs.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
<!DOCTYPE html>
<html>
    <head>
        <title>翻訳</title>
        <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
 
    </head>
    <body>
        <style>
          t0 {color: blue; font-size:20px;}
          t1 {color: red; font-size:20px;}
         img.gazo{
               width: auto;
               height: auto;
               border:solid 5px #0000FF;
               clear:both;
               }
        </style>
     </body>
 
 
<?php
// APIキー
define("APIKEY","①");
set_time_limit(60*60);//タイムアウト時間を60分に変更
 
//翻訳関数
function trns_us($text)
{
    //言語の検出
    $url = "https://www.googleapis.com/language/translate/v2/detect?key=" . APIKEY . "&q=".urlencode($text);
    $resjson = file_get_contents($url);
    $resjson = mb_convert_encoding($resjson, 'UTF-8', 'auto');
    $resarray = json_decode($resjson);
    $lang = $resarray->data->detections[0][0]->language;
//print_r($result);
 
     
     
    $url = "https://www.googleapis.com/language/translate/v2?key=" . APIKEY . "&source=".$lang."&target=ja&q=".urlencode($text);
    $resjson = file_get_contents($url);
    $resjson = mb_convert_encoding($resjson, 'UTF-8', 'auto');
    $resarray = json_decode($resjson);
    $result = $resarray->data->translations[0]->translatedText;
//    echo $result;
    return $result;
}
 
 
$file  = isset($_COOKIE["file"])? $_COOKIE["file"] : "";
if ( $file == "" ) {
    echo "画像を取得してください".PHP_EOL;
}
else
{
    $file = $_COOKIE["file"];
    echo     $file .PHP_EOL;
    echo "<img src=test.PNG  class=gazo>";
 
    //ブラウザに少しずつ表示するため必要
    ob_flush();
    flush();
         
    // Feature Type
    $feature = 'TEXT_DETECTION';
    // パラメータ設定
    $param = array("requests" => array());
    $item["image"] = array("content" => base64_encode(file_get_contents($file)));
    $item["features"] = array(array("type" => $feature));
    $param["requests"][] = $item;
 
    // リクエスト用のJSONを作成
    $json = json_encode($param);
    // リクエストを実行
    $curl = curl_init() ;
    curl_setopt($curl, CURLOPT_URL, "https://vision.googleapis.com/v1/images:annotate?key=" . APIKEY);
    curl_setopt($curl, CURLOPT_HEADER, true);
    curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "POST");
    curl_setopt($curl, CURLOPT_HTTPHEADER, array("Content-Type: application/json"));
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_TIMEOUT, 30);
    curl_setopt($curl, CURLOPT_POSTFIELDS, $json);
    $res1 = curl_exec($curl);
    $res2 = curl_getinfo($curl);
    curl_close($curl);
 
    echo "<br>";
    // 取得したデータ
    $json = substr($res1, $res2["header_size"]);
    $data = json_decode($json, true);
    $str = $data["responses"][0]["textAnnotations"][0]["description"];
    $str2 = str_replace(array("\r\n","\n","\r"), " ", $str);//改行をなくす
    echo "<t0>" .  str_replace(". ", ".<br>", $str2) . "</t0>";
    echo "<br>";
    $str3 = trns_us($str2);//翻訳
 
    $str3 = str_replace("。", "。<br>", $str3);//表示は改行ありで。
    echo "<t1>" . $str3  . "</t1>";
    echo "<br>";
 
 
}
 
//http://localhost/ocr_trs.php
?>
 
<script type="text/javascript">
    var fname = "";
    $.ajax({
        url : "filechk.php",
        type : "POST",
        data : {    post_data_1:1,
                    }
    }).done(function(data,text) {
        console.log("ajax通信に成功しました");
        fname = data;
//        console.log(fname);
        document.cookie = 'file='+fname;
        location.reload(true);
    }).fail(function(xhr, textStatus, errorThrown) {
        console.log("ajax通信に失敗しました");
        location.reload(true);
    });
  </script>
</html>




注釈として
24行目、①の部分に自身のAPIキーを入力します。
25行目、PHPで長時間待つために必要
31行目から35行目。言語の検出を行わない場合は$langをその言語に入力してこれらをコメントアウトしてください。
57行目 ファイル名を表示してますが邪魔ならコメントアウト
65行目 文字認識を指定しています。
68行目 画像データをbase64という文字列に変換しています。
93行目 改行を一度なくしたあと、94行目でピリオドを改行に変換しています。
98行目 翻訳結果のピリオドを「。」に変換しています。
110行目から javascriptからajax通信を使いphpから新規のスクリーンショットのファイル名を取得しています。

2018年7月25日水曜日

変体仮名コンバート

基本的な処理は変体仮名タイピングと同じです。
テキストエリアの文字のひらがなを変体仮名(またその字母)に置き換えます。
変体仮名フォントを自分のPCにインストールする必要があります。
説明(変体仮名タイピングの挙動とほぼ同じ)

下記画像のようになります。


これ以下が本体です。

2018年7月24日火曜日

変体仮名をブログ上で表示したいぞ!(解決編)

やりたいこと。
フォントは個人個人が「Koin変体仮名明朝f2」を用意して、
ブログ上で変体仮名が表示できる状態。

今回の解決方法

※外字を活用していると上書きとなるかもしれません。そういう方は退避が必要です。
「Koin変体仮名明朝f2」のフォルダ内にある「Koin変体仮名明朝f2.tte」を
「C:\Windows\Fonts」に放り込みます。フォントのインストールが始まります。。
そうすると「EUDC」というフォントができます。
※今回の使用方法では、変体仮名フォントを外字エディターでリンクする必要はありません。

②コード上で
font-family: "EUDC","MS 明朝";
とします。後半の"MS 明朝"は字母を等幅フォント表示するため。

③IEのみ改行コードが改行はするものの「・」(トーフ?って奴かも)と
余計なものが表示されてしまいます。。
挙動として"EUDC"の直後の改行のようです。
そのため、

1
2
t1 { font-family: "MS 明朝";}
var kaigyou ="<t1><br></t1>";

として改行のフォントを変更することによって一応の回避はできます。

※環境:Windows7 64bit IE11、Chrome、Firefox

以上、スマートではないのかもしれませんが、WEBで外字フォントを使用の件は解決しました。

2018年7月15日日曜日

変体仮名タイピングver1.1





変体仮名タイピング

変体仮名タイピング

スペースキーを押して開始!
クリア数:0
誤タイプ:0
問題選択   : あ行か行 さ行た行 な行は行 ま行や行 ら行わを 百人一首からなど10問
表示モード   : 字母のみ 変体仮名のみ 両方
ローマ字表示 : あり なし

変体仮名タイピング説明

アンドロイドの変体仮名のアプリはあるものの、いまいちどうやって勉強していいかわかりません。
暗記なのか、書いて覚えるにしても変体仮名の書き順がわからないなど。
とりあえず字形から反射的に思いつくようになるかなという感じでタイピングゲームを作ってみました。
字母⇒変体仮名と変体仮名⇒字母どちらで覚えるのがいいのだろうか?



プレイはこちらから


使用方法
0:koinさんの変体仮名のフォント[Koin変体仮名外字明朝(フリー版)]を使用します。
※外字を活用していると上書きとなります。そういう方は退避が必要です。
「Koin変体仮名明朝f2」のフォルダ内にある「Koin変体仮名明朝f2.tte」を
「C:\Windows\Fonts」に放り込みます。フォントのインストールが始まります。
そうすると「EUDC」というフォントができます。
※今回の使用方法では、変体仮名フォントを外字エディターでリンクする必要はありません。


1:問題やモードを選択
2:問題初期化ボタンを押す
3:スペースキーを押してスタート
4:左から順に字母や変体仮名に対応したローマ字で入力する
(ローマ字は答えそのものなので、デフォルトでは右端に表示してます)

問題仕様
濁音、半濁音、小さい「っ」や「ょ」などは無し
「ん」は変体仮名フォントに割り当てがない
「百人一首からなど10問」は、百人一首から適当に選んだ言葉と適当に思いついた言葉からランダムに10問出題。
例として、「か」だったら変体仮名が3種類あるので(字母でいうと「加」「可」「嘉」の3種類)だが連続して同じ文字がでないようになっている。
始まりはランダムで順繰り割り当て。頭にきやすい変体仮名とかがあるようですが関係なく割り当てします。
それ以外の問題は、変体仮名を一文字ずつ割り当てがある分すべて出題します。
同じ字母で字形が違う変体仮名がこのフォントセットにあるため同じ字母が出ます。

(自分向けに)タイピングしやすいようにローマ字は以下のMAPで製作しています。
訓令式に近い感じです(「を」をwoとしています)



コード仕様:
javascriptを使用
変体仮名は「Koin変体仮名外字明朝(フリー版)」を使用。
ファイル分割(.jsファイル)はブログ版ではあきらめた
ブログに上げた場合、変体仮名がなぜかコード内では8文字扱いになっているため
「ん」と混在した時に処理が煩雑になるので「ん」を「百人一首からなど10問」から削除。

いつのまにかコード表示しなくなってる\(^o^)/

そりゃ、レイアウト変えたらカスタム部分に書いたSyntaxHighlighter消えますね
その上、bodyとheadを勘違いして悩むしさぁ 
 \(^o^)/

これで復活したはず

2018年6月17日日曜日

一度気にするとすごく気になる

バーチャルユーチューバーを見ていて、アレって思うことがあったのですよ。

バーチャルユーチューバーのあさひ
アマザンプライマーのピーナッツくん
の二人がそれでした。


それは「まばたき」です。
この二人はまばたきするときの上まぶたと下まぶたの移動が50:50です。
まぁピーナッツくんは人間じゃないのでいいとして哺乳類は基本上まぶたしか動かないそうです。

あとは富士葵、モスコミュール、かしこまりはちょっと動きが気になるかも。
急に下のまつげが現れるみたいな?そのせいで下まぶたが大きく動いてる感じがします。
(ちなみに自分で鏡を見て片目つぶったり、ウィンクしたりだと下まぶたが動いちゃいます)


登録しているchの人をざっとみた感じだと
感覚的には上まぶたのみか、90:10ぐらいのバーチャルユーチューバーが多いかなぁと思います。
たぶんデフォルメというか(≖ ‿ ≖)や( ◠ ◡ ◠ )みたいな目になる関係でしょうがないのかもしれないですね、
おそらく、3Dでも2Dでセオリーや、うまく完全につぶるときにサッとごまかすテクニックがあるのでしょう。

まばたきと口が動いてないと生きている感がないからね。

※敬称略。ざっと見ただけなので最新のものでは修正されているかもしれませんね。
////////////////////////////////////////////////////////////////////
別の話題。
今現在感じていることをメモする

最近のバーチャルユーチューバーは
バーチャルなタレント

バーチャルなユーチューバー
って感じに分かれつつある感じがしますね。
そうなるとタレント方面の方はいつかバーチャルユーチューバーっていう
名称とお別れする日がくる気もしますねぇ。

あとTVに出るとしたらバーチャル枠として割り当てられるのかな。
タレントで考えると、オカマ枠や物知り枠、悪魔枠とか学者枠とかあるのだけど、
バーチャルタレントのオカマ枠とか悪魔枠とか学者枠で割り当てられるイメージが今現在は無いですね…学者枠とか本当に知識があるか裏付けがね…


ついでにさらに別の話題
ちゃんと番組構成というか構成考えている生放送は見やすいですね。安心で見られる
バーチャルユーチューバーを見ることによって、いままで見てきたTV番組ってよくできているなぁと認識しました。

どうせ、バーチャルユーチューバーって追える量じゃないのですから
バーチャルユーチューバーTVのとある1コーナーをたまに見るという心もちで見ようと思います。
ほぼ構成なしのダラダラ生放送で、偶然がかみ合って超おもしろくなるのを期待するほど今時間が余っていないのです。

2018年5月26日土曜日

YouTubeの生放送のチャットを使ってアンケートを行う


追記
youtubeライブに2021年ごろ?に便利なアンケート機能が追加されました。
追記終わり

さてタンバリンさんのツールが動いているのを見ました。
あれは過去のコメントも保存しているようですね。それどうだろうなぁ?
アンケート機能があるようですね。便利そうですね。
じゃあYouTubeの生放送のチャットを使ってアンケートを作ってみましょ


ニコニコのようなアンケート機能が視聴者には匿名ですしやりやすい思うのですが、
YouTubeにはそのような機能はないのでお試しに作ってみました。

検索すると、YouTubeでアンケートカードというものもあるらしいですが、
生放送には対応していないのかな?誰か作ってそうですけどねぇ




仕様は、
・投票項目を入力できる
・投票開始ボタンが押されてからのチャットのみを投票の対象とする
・アンケート中に投票数が確認できる
・IDで同じ人からの投票を弾く(途中で投票先の変更はダメ)
・投票終了時の結果を円グラフで表示する。
※スーパーチャットには今回未対応

苦労した所は、
ほんとはしちゃいけないだろうけど、PHPのコード部分に無限ループする部分があります。
ブラウザがChrome
かつ
sessionを使用した場合
スクリプトが終了していないのと次のページ遷移できない
という問題があるようで解決に時間がすごくかかりました。
(IE,FireFoxは停止しなくても遷移しました。)
解決策として正しいかわからないですが、
window.stop();
によってブラウザの中止ボタンの機能によりスクリプトを停止して解決してます。
そしたら今度は、IEがwindow.stop()関数に対応していないので使用ブラウザの判断をして
回避しています。
これでIE,Chrome,FireFoxで動いているのを確認しました。

3ページ分のコードをいつものフォルダ(C:\xampp\htdocs)に配置して
ブラウザ上で
http://localhost/vote1.php
とすれば使用できます。
いままでのやってきたことを組み合わせたようなもんです。


外見はこんな感じです。
見れば動きはなんとかくわかるでしょう。
1ページ目
例は雑に1文字ずつになっていましが、ちゃんと指定できます。
これは投票先を「あ」「い」「う」にしています。
本来は好きなお菓子の名前などちゃんとしたものを入れましょう。
2ページ目
投票項目が見えるようにしています。
この画像の動作では「あ」「い」「う」のワードを含むチャットを「投票した」
としていますが、
一致でもいいし、
投票項目のワードが長いと入力が大変だと思い「#1」~「#3」でもコードをいじればできます。(数字だけでもいいでしょう)
何も表示されないと寂しいのでチャットと現在の投票数を表示しています。
アンケートを打ち切る場合は投票終了ボタンを押します。

3ページ目
HighChartsを使いアンケート結果が円グラフで表示されます。
それぞれの結果を%と票数も全部載せです。
再びアンケートをやる場合は1ページに目から開始してください。




コードです。
1ページ目
vote1.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang = "ja">
  <head>
    <meta charset = "utf-8">
    <title>アンケート項目設定</title>
  </head>
<body>
<form action = "vote2.php" method="post">
<p>投票項目を入力してください<br>
<textarea name="keyword" cols="60" rows="6"></textarea>
<br>
<input type = "submit" value="投票開始" name="button1">
</form>
</body>
</html>


form部分だけなので注釈はありません。
php部分が1行もないのにファイル名がphpですね。。。


2ページ目
①にはAPIキー、②には今回は生放送中の動画のidを入れます。
https://www.youtube.com/watch?v=xxxxxxxxxxx
のxxxxxxxxxxxの文字列のことです。




vote2.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
<!DOCTYPE html>
<html lang = "ja">
  <head>
    <meta charset = "utf-8">
    <title>アンケート中</title>
    <script language="javascript" type="text/javascript">
        function OnButtonClick()
        {
         var userAgent = window.navigator.userAgent.toLowerCase();
         console.log(userAgent);
        if(userAgent.indexOf('msie') != -1 ||
           userAgent.indexOf('trident') != -1 )
        {
          //IEは何もしない
        }
        else
        {
          window.stop();
        }
        document.frm.submit();
        return false;
       }
    </script>
  </head>
 
  <body>
        <style>
          t0 {color: blue; font-size:16px;}
        </style>
<form name="frm"  action = "vote3.php" method="post">
<p>投票項目     
<input type="button" onclick="OnButtonClick()" value="投票終了" name="button2">
</p>
</form>
 
<?php
session_start();
session_unset();
 
set_time_limit(60*60);//タイムアウト時間を60分に変更
 
if(isset($_POST["button1"]))
{
 
    if($_POST["keyword"] !=  "")
    {
        //投票項目の作成
        $str = str_replace(array("\r\n","\r","\n"), "\n", $_POST["keyword"]);
        $tmplist = explode("\n", $str);
        $length = count($tmplist);
        $keylist = array();
        for($i=0;$i<$length;$i++)
        {
            if($tmplist[$i] != "")
            {
                array_push($keylist,$tmplist[$i]);
            }
             
        }
        //投票数をクリア
        $cnt  = array_fill(0,count($keylist),0);
 
        //投票先を表示
        foreach($keylist as $t => $keyword)
        {
            $chkkey = "#".((int)$t+1) .": ". $keyword;
            print_r($chkkey);
            echo "<br>";
            }
        echo "<hr>";
 
        //セッションに保存する(初期化)
        $_SESSION["chart"] = 0;
        foreach($keylist as $k => $keyword)
        {
            $_SESSION["key".$k] = $keyword;
            $_SESSION["cnt".$k] = $cnt[$k];
 
        }
        $_SESSION["num"] = count($keylist);
    }
    else
    {
        print_r("投票項目をいれてください");
        goto end;
    }
}
else
{
    print_r("エラー");
    goto end;
}
 
$apikey = "①";
$videoId = "②";
 
 
 
//ChatIdの取得
$search_api="https://www.googleapis.com/youtube/v3/videos?part=liveStreamingDetails&id=" . $videoId ."&key=" . $apikey;
$search_contents = file_get_contents($search_api);
$search_json = json_decode($search_contents,true);
$ChatId = $search_json["items"][0]["liveStreamingDetails"]["activeLiveChatId"];
 
//ChatIdからliveChatにアクセス用のURL
$search_api="https://www.googleapis.com/youtube/v3/liveChat/messages?part=snippet,authorDetails&liveChatId=" . $ChatId ."&key=" . $apikey;
 
 
$idlist = array();//回答者のIDリストのクリア
$next = "";
$url = $search_api;
$skip = 0;
while(1)
{
    if($next)
    {
        $url = $search_api . "&pageToken=" . $next;
    }
     
    if($search_contents = @file_get_contents($url))
    {
        $search_json = json_decode($search_contents,true);
        $time_start = microtime(true);
 
 
        foreach($search_json["items"] as $k => $data)
        {
 
            //初回は過去データなのでスキップ
            if($skip == 0)
            {
                $skip = 1;
                break;
            }
            $text = $data["snippet"]["displayMessage"];
            print_r($text);
            echo "<br>";
 
            $trgid = $data["authorDetails"]["channelId"];
            foreach($keylist as $t => $keyword)
            {
                //$keyword = "#".((int)$t+1);
                //if($text == $keyword)
                //if($text == "#".((int)$t+1))
                if(strpos($text,$keyword) === false){}else
                {
                     
                    if(in_array($trgid, $idlist) == false)//重複者チェック
                    {
                        array_push($idlist,$trgid);
                        $cnt[$t] = $cnt[$t] + 1;
                        //セッションの値を更新する
                        $_SESSION["cnt".$t] = $cnt[$t];
                    }
                    break;
                }
            }
        }
        $next = empty($search_json["nextPageToken"])? 0 : $search_json["nextPageToken"];
 
 
        $delay = $search_json["pollingIntervalMillis"]/1000.0;
        $time = microtime(true) - $time_start;
         
 
        echo "<t0>現在の投票数" .  count($idlist) . "</t0>";
        echo "<br>";
         
        //ブラウザに少しずつ表示するため必要
        ob_flush();
        flush();
 
        do{
            $time = microtime(true) - $time_start;
            usleep(100);//0.1ms待ち
        }while($time < $delay);
         
    }
}
 
end:
?>
 
  </body>
 
 
</html>


注釈として
7行目から22行目あたりがスクリプトを停止して3ページ目へ遷移させる部分です。
48行目から59行目あたりが1ページ目の入力を分解して投票項目を作成しています。
空の行を無視します。
94行から下は基本的にはチャット表示のコードの改造です。
130行目あたりは、APIが初回の入力はボタンを押したものの過去のチャットとなりますのでスキップする処理です。
142行から145行あたりは、一致するか、含むにするか、#nの一致にするかです。
好きなコードを選んでください。
148行目から156行目はまだ投票していないIDでの投票一致の場合、票として数えているところです。

3ページ目
vote3.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<!DOCTYPE html>
<html lang = "ja">
  <head>
    <meta charset = "utf-8">
    <title>アンケート結果</title>
<script type="text/javascript"  src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script type="text/javascript"  src="https://code.highcharts.com/highcharts.js"></script>
<script type="text/javascript"  src="https://code.highcharts.com/modules/exporting.js"></script>
  </head>
  <body>
 
<form>
<div id="container" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
</form>
 
 
<?php
session_start();
if($_SESSION["num"]  !="")
{
    $_SESSION["chart"]  = 1;
}
else
{
    print_r("エラー");
}
 
?>
 
 
 
<script>
var phpSession = <?php echo json_encode($_SESSION); ?>;
 
if(phpSession["chart"] ==1)
{
Highcharts.chart('container', {
    chart: {
        animation: Highcharts.svg, // don't animate in old IE
        plotBackgroundColor: null,
        plotBorderWidth: null,
        plotShadow: false,
        type: 'pie'
    },
    title: {
        text: 'アンケート結果'
    },
    tooltip: {
        pointFormat: '{series.name}: <b>{point.percentage:.1f}%</b>'
    },
    plotOptions: {
        pie: {
            allowPointSelect: true,
            cursor: 'pointer',
            dataLabels: {
                enabled: true,
                format: '<b>{point.name}</b>: {point.percentage:.1f} % :{point.y}票',
                style: {
                    color: (Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black'
                }
            }
        }
    },
    series: [{
        name: 'vote',
        colorByPoint: true,
 
    data: (function () {
      var data = [],i;
        for( i=0;i<phpSession["num"];i+=1)
       {
        data.push({
               name:  "#"+(i+1)+": "+phpSession["key" + i],
               y: phpSession["cnt"+i]
        });
      }
      return data;
    }())
 
 
 
    }]
});
}
</script>
 
  </body>
 
</html>



注釈として
Highchartsの円グラフの例 を改造し、2ページ目の結果を入力としています。
33行目でPHPから_SESSIONのデータを受け取り、
それを68行目から78行目で使用しています。



ちゃんと勉強せず、行き当たりばったりで作っているから苦労する。
いままでのコードもformやhead、bodyの対応がよくなかったようだ。
WEB系の基本勉強してないものなぁ。

2018年5月18日金曜日

マウスのチャタリングを確認する

 マウスが壊れてシングルクリックがダブルクリックになったり、ドラッグ中に離した判定になったりすることがあるじゃないですか。でも指が疲れているせいか本当に壊れいてるかはっきりなしないことがありますよね。


そこで Highcharts(グラフ)を使って可視化してみました。JavaScriptを使いマウスイベントをグラフに反映します。いつも通りやりかたは雑いです。
画像の赤い部分でマウス操作するとグラフに反映されます。
コードを少し変更すればクリックとダブルクリックが確認できます。

押してしばらくして離すを繰り返した結果が以下です。
[追記:ためしにブログ上においてみました]

 マウスダウン(押す)すると青の点が1になり、マウスアップ(離す)と黒の点が2になります。
上記の図だと離したときにチャタリングおこって意図していない結果になっています。

左ボタンを確認したら、


マウスのプロパティを左きき用にして右ボタンを確認してもいいでしょう。
(変更せずとも反応しますが、右クリックだとメニューが出るのでイベントを拾えているか不安…)
下記がコードです。htmlファイルとして保存してブラウザで表示すれば使えます。
外部のリソース(Highcharts)を使っているけどいいんかな?


ClickChart.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
<!DOCTYPE html>
<html lang = "ja">
  <head>
    <meta charset = "utf-8">
    <title>マウスチェック</title>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/exporting.js"></script>
     
<div id="container" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
<div id="text-button" onmousedown="fMouseDown(this)" onmouseup="fMouseUp(this)" style="background-color: red;"><p id="text">クリック</p></div>
  </head>
  <body>
   
<script>
var input_x0=Array(32);
var input_x1=Array(32);
var x0_rp = 0;
var x0_wp = 0;
var x1_rp = 0;
var x1_wp = 0;
 
document.getElementById("text-button").fMouseDown = function() {
    var x = (new Date()).getTime();
    input_x0[(x0_wp)] = x;
    x0_wp = (x0_wp +1)%32;
}
 
document.getElementById("text-button").fMouseUp = function() {
    var x = (new Date()).getTime();
    input_x1[(x1_wp)] = x;
    x1_wp = (x1_wp +1)%32;
}
 
/*
document.getElementById("text-button").onclick = function() {
    var x = (new Date()).getTime();
    input_x0[(x0_wp)] = x;
    x0_wp = (x0_wp +1)%32;
}
 
document.getElementById("text-button").ondblclick = function() {
    var x = (new Date()).getTime();
    input_x1[(x1_wp)] = x;
    x1_wp = (x1_wp +1)%32;
}
*/
 
Highcharts.setOptions({
  global: {
    useUTC: false
  },
    lang: {
        numericSymbols: null//軸のkキロ表示をやめる
    }
});
 
Highcharts.chart(&#039;container', {
  chart: {
   type: &#039;line',
    animation: Highcharts.svg, // don't animate in old IE
    marginRight: 100,//右の軸が隠れるので適切な値にする
    events: {
      load: function () {
        var series0 = this.series[0];
        var series1 = this.series[1];
 
        setInterval(function () {
        var x = (new Date()).getTime(); // current time
//series0に対する処理
        var p = x0_wp;
        while (x0_wp != x0_rp) {
         series0.addPoint([input_x0[x0_rp], 1], true, true);
         series0.addPoint([input_x0[x0_rp]+1, 0], true, true);
          
         series1.addPoint([input_x0[x0_rp], 0], true, true);
         series1.addPoint([input_x0[x0_rp]+1, 0], true, true);
         x0_rp = (x0_rp +1)%32;
        }
//series1に対する処理
        p = x1_wp;
        while (p != x1_rp) {
         series1.addPoint([input_x1[x1_rp], 2], true, true);
         series1.addPoint([input_x1[x1_rp]+1, 0], true, true);
          
         series0.addPoint([input_x0[x1_rp], 0], true, true);
         series0.addPoint([input_x0[x1_rp]+1, 0], true, true);
         x1_rp = (x1_rp +1)%32;
        }
       }, 500);//0.5秒のインターバルタイマ
      }
    }
     
  },
  title: {
    text: &#039;チャタリングチェック(赤い部分をクリックでチェック)'
  },
  plotOptions: {
        series: {
         marker: {
                enabledThreshold: 0//マークが見えるように
            }
        }
    },
  xAxis: {
    type: &#039;datetime',
    tickPixelInterval: 150
  },
  yAxis: [
      {
          title: {
              text: &#039;クリック'
          },
          plotLines: [{
              value: 0,
              width: 1,
              color: &#039;#FF8080'
          }],
          allowDecimals: false    //軸を整数表示
      },
  ],
  tooltip: {
    formatter: function () {
      return &#039;<b>' + this.series.name + '</b><br/>' +
        Highcharts.dateFormat(&#039;%Y-%m-%d %H:%M:%S', this.x) + '<br/>' + this.y;
    }
  },
  legend: {
    enabled: true    //凡例表示
  },
  exporting: {
    enabled: false
  },
   
  series: [{
    name: &#039;マウスダウン',
    yAxis: 0,
    data: (function () {
      var data = [],
        time = (new Date()).getTime(),
        i;
 
      for (i = -19; i <= 0; i += 1) {
        data.push({
          x: time + i * 500,
          y: 0
        });
      }
      return data;
    }())
  },
  {
    name: &#039;マウスアップ',
    yAxis: 0,
    data: (function () {
      var data = [],
        time = (new Date()).getTime(),
        i;
 
      for (i = -19; i <= 0; i += 1) {
        data.push({
          x: time + i * 500,
          y: 0
        });
      }
      return data;
    }())
   },
   ]
});
 
 
</script>
 
 
  </body>
</html>


注釈として、
マウスのイベントとタイマーのイベントが重なっても大丈夫なようにリングバッファでイベントの時間を格納しています。
イベントのタイミングでグラフに直接反映できればいいのですが、JavaScriptよくわからないのであきらめました。
23行目から33行目をコメントアウトして35行目から46行目を反映させればクリックとダブルクリックのイベントに変更できます。
70行目から88行目、ダウンもアップも同じ数が発生するようにしています。そうしないとX軸は全部のポイントを表示するようにしているためその対策です。
※アップとダウンはペアなので問題ないが、クリックとダブルクリックは困る。
またX軸の時間は可変なので時間がながい細かい部分がみづらくなります。対策はあきらめました。
101行目 値ありから0にすぐ戻しているため、デフォルト設定だとマークが近すぎでません。その対策で0を設定しています。
ちなみに今回は線ありでやっていますが、線の幅を0にするとマークだけになります。


ブログで直接実行もできるけどやめとこ。
マウスの調子が悪いので思いつきで作ってみました。明らかに壊れてるわ

2018年5月16日水曜日

片鱗が見えたかなぁ

昨日のタンバリンさんのyoutubeコメント拾いマシーン(名前前回と違うじゃん)の部分を見て思ったこと。

更新がうまくっている部分を見ていると
2秒で4コメントぐらい追加されているように見える。
 ・一度API投げて保存したコメントを吐き出しているとしたら、人気バーチャルユーチューバーさんの場合ピークでは170コメント/9.5秒(実測)なので処理量としては追いつかない。
 ・停止したときにアイコンは違うけど名前とコメントが同じ人が表示されているからなんかバグっている。
ということがわかりますね。

 2秒毎にAPIを投げているということはなさそう。なぜならpollingIntervalMillisを無視することになるのでwarningを出てコメントを取得できない。保存したコメントを少しずつ出しているのでしょう。

 発言から動作テスト時は動いていたみたいですが限定公開でリスナーなしでやっていたのかもしれませんね。その場合はコメントが多い生放送してるときと差があります。
 pollingIntervalMillisは5秒以下で短いですし、最初のAPIでだいたい過去のコメント75ぐらいが一気に取得してしまいます。
 放送を見る限りpollingIntervalMillisが動作テスト時と違い長すぎて止まったと勘違いしたってことはなさそうです。
  適切なpollingIntervalMillisをwaitしてAPIを使用しているのか、それとコメントを保存している部分に不具合があるような気がしますねぇ。


Unityは見栄えがいいなぁ。アイコンも丸くなっているし。フィルタしてキーワードのだけ表示じゃなくて全部保存してから検索って形なのかしら 。

テスト時と実際の時の差異によるバグってなんかわくわくしますね。

2018年5月11日金曜日

YouTubeチャットで視聴者参加のためのツールを考える

5/8の某生放送でバーチャルユーチューバーのマネージャーのタンバリンさんが言っていたYouTubeのコメント欄で視聴者参加できるWPP1システム?というやつを想像で作ってみる。
PHPじゃなくてもっと立派なものだろうと思うけどね。
PHPほんと動的につかいづらい
っていうか、上位のバーチャルユーチューバーってチャット欄が人数に対して、速すぎて読めなくない


参加者を募る(キーワードを決めてそれをコメントした人のみを抽出)

参加者のコメントのみを表示する
っていう機能があればよさそう。

初期画面


モード1:決めたキーワードをコメントした人だけが表示される(今回のコードではキーワードを含むにしている。本来は一致だろう)
例はキーワードの所に「あ」を入力しkeyボタンを押した場合。
「あ」を含むコメントした人だけが表示されている。


 アイコン、ID(青字)、名前(赤字)、コメント(黒字)の順に表示されるので、青字のIDをコピーしてIDに入力しIDボタンを押す。


モード2:対象IDの方のみチャット が表示される
対象ボタンを押した先に遷移します。(入力欄が空白の場合は初期画面に遷移)

 コードは以下の通り。①にはAPIキー、②には生放送中の動画のidを入れます。
https://www.youtube.com/watch?v=xxxxxxxxxxx
のxxxxxxxxxxxの文字列のことです。
今回はアイコンも表示したいのでブラウザで無理やり表示してます。
ブラウザで「http://localhost/modoki.php」と入力。



modoki.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<!DOCTYPE html>
<html>
    <head>
        <title>もどき</title>
    </head>
    <body>
        <style>
          t0 {color: blue; font-size:16px;}
          t1 {color: red; font-size:32px;}
          t2 {color: black; font-size:32px;clear: both;}
         img.gazo{
               width: 44px;
               height: auto;
               float: left;
               }
        </style>
         
<form action = "modoki.php" method="post">
<p>キーワード:<input type="keyword" name="keyword">
<input type = "submit" value="key" name="button1"></p>
<p>ID:<input type="userid" name="userid">
<input type = "submit" value="ID" name="button2"></p>
<?php
set_time_limit(60*60);//タイムアウト時間を60分に変更
$mode = 0;
if(isset($_POST["button1"]))
{
    if($_POST["keyword"] !=  "")
    {
        $mode = 1;
        $keyword = $_POST["keyword"];
        echo "キーワード:" . $keyword;
    }
}
else if (isset($_POST["button2"]))
{
    if($_POST["userid"] != "")
    {
        $mode = 2;
        $chkid = $_POST['userid'];
        echo "対象ID:" . $chkid;
    }
}
$key = "①";
$videoId = "②";
//ChatIdの取得
$search_api="https://www.googleapis.com/youtube/v3/videos?part=liveStreamingDetails&id=" . $videoId ."&key=" . $key;
$search_contents = file_get_contents($search_api);
$search_json = json_decode($search_contents,true);
$ChatId = $search_json["items"][0]["liveStreamingDetails"]["activeLiveChatId"];
//ChatIdからliveChatにアクセス用のURL
$search_api="https://www.googleapis.com/youtube/v3/liveChat/messages?part=snippet,authorDetails&liveChatId=" . $ChatId ."&key=" . $key;
$next = "";
$url = $search_api;
while(1)
{
    if($next)
    {
        $url = $search_api . "&pageToken=" . $next;
    }
     
    if($search_contents = @file_get_contents($url))
    {
        $search_json = json_decode($search_contents,true);
        $time_start = microtime(true);
        foreach($search_json["items"] as $k => $data)
        {
            $exsit = 0;
            $text = $data["snippet"]["textMessageDetails"]["messageText"];
            $trgid = $data["authorDetails"]["channelId"];
            if($mode == 1)
            {
                if(strpos($text,$keyword) === false)
                {
                    //見つからない場合なにもしない
                }
                else
                {
                    $exsit = 1;//指定文字列が含まれていた場合
                }
            }
             
            if($mode == 2)
            {
                if($trgid==$chkid)
                {
                    $exsit = 1;
                }
            }
            if($exsit == 1)
            {
                echo "<p>";
                echo "<t0>" .  $trgid . "</t0>";
                $image = $data["authorDetails"]["profileImageUrl"];
                echo "<img src=$image  class=gazo>";
                echo "<t1>" . $data["authorDetails"]["displayName"] . "</t1>";
                echo "<t2>";
                $text = $data["snippet"]["textMessageDetails"]["messageText"];
                echo $text;
                echo "<t2>";
                echo "</p>";
            }
        }
        $next = empty($search_json["nextPageToken"])? 0 : $search_json["nextPageToken"];
        //ブラウザに少しずつ表示するため必要
        ob_flush();
        flush();
        $delay = $search_json["pollingIntervalMillis"]/1000.0;
        $time = microtime(true) - $time_start;
         
        do{
            $time = microtime(true) - $time_start;
            usleep(100);//0.1ms待ち
        }while($time < $delay);
    }
}
?>
     </body>
</html>


注釈としては、
7行目から16行目で文字の装飾と画像のサイズを指定しています。
サイズを指定してそれっぽく表示 。うまくできなくてえらい時間がかかった。
24行目。PHPの処理は無限ループでまわしているため処理が終了しません。
ブラウザ上の場合は30秒でタイムアウトの処理により停止するようなので60分に延長しています。
26行目から43行目(組込み系のコードだと空のelse書くものだけど忘れた)がボタンを押した場合の処理です。
ボタンが押された場合、どのボタンが押されたかと二つのテキスト入力の値が$_POSTに格納された状態で再度同じPHPが呼び出されます。
( 18行目で自身を呼び出すように設定しています。)
62行目 file_get_contenstsでwarninngが返った時の対策に@をつけてみましたが、
これで対策うまくいってるのかなぁ
73行目 キーワードを含む場合のチェック。キーワードと一致に変更するなら両者を==で比較する
85行目 対象IDかをチェック
106,107行目 ブラウザ上の場合、通常PHPの処理すべてが終了しないと表示されないが、無限ループ(55行目)なので途中の状態を表示するための関数呼び出しです。
92行目から101行目がコメントとアイコンの表示部分。IDはモード2で出力しなくていいかも。
追記:2018/5/12
スーパーチャットには$data["snippet"]["textMessageDetails"]["messageText"]は存在しない。本当は対応が必要。共通としては$data["snippet"]["displayMessage"]とすると楽だと思うがスーパーチャットの場合「¥1,000 from 名前 : ”コメント”」のような構造なのでキーワードの一致で判定しにくいので場合わけが必要だろう。
[snippet][type] でsuperChatEventならば[snippet] [superChatDetails][userComment]を$text。textMessageEventならば["snippet"]["textMessageDetails"]["messageText"]で一致判定がOK。あと、98行目は重複しているのでいらない。
思えば取り込み後に削除されたコメントも保持してしまう。既に削除されているならば上記の判定で無視できるはずだ。その辺は運用で対応。

やりたいことはこんな感じだろうけど、右クリックで対象者選択とかもっと使いやすいのだろうなぁ。配布したりするのだろうか。
PHPって不向きだと思う。
本当はモード1にID表示なくして何番の人か指定でIDを保存していた配列から出力したかったけどPHPの処理が無限ループで完了しないからPOSTに渡せなそうなのであきらめたり、レイアウト関係がむずかしくてあきらめたりと妥協しまくって。このコード量でそれでも1日かかった。まぁ自分C言語の人なので。

2018年4月29日日曜日

いくつかのバーチャルYouTuberのタグを覗いてみて感想 とついでに愚痴


いくつかのバーチャルYoutuberのタグを覗いてみました。
APIを使わずともソースの表示を見ることができるのですが、APIの方が便利で早いのです。
なんというか、配信者側が自分で「かわいい」とか「ロリ」とか書いてあるのって
なんか見てはいけもないモノを見た恥ずかしさを感じてしまいますね。

傾向として
企業勢は割としっかりやっているけど一部個人勢はタグ無しの人もいて、
せめてコラボした人の名前とかモノマネした人の名前入れてあげてって思う。
それができている人は、動画やその他の対応もしっかりしている感じもする。
効果のほどがどれだけあるか知りませんが関連動画に影響があるのでしょう?
生放送はついてない場合が多いのが勿体なぁ。

ある程度濁して書くと。
・今年に入ってからタグにバーチャルYouTuberって入れたんだな。あと数か国語で「かわいい」が入っているし戦略的。最初から「crazy」って入っているのは好感。
・清楚・イルカってタグにはないのか・・・
・ずっとイラストレーターの名前を入れているとは義理堅いなぁ
・動画内容と無関係なのに上位のバーチャルYoutuber名前入れるのはいかんのでは?
2名見つけたけどどちらも登録者数1万超えているのだからそんなのいらないんじゃねーの。
「新人」とか「初投稿」も入っているしコピペかな?
・さすがにパクリもとのタグはつけんか
・割と初期に自分でクソメガネってタグいれとる・・・あっ、もう一人クソメガネって入れている人がいた。
・○○勢と一括りにされるけど人によってタグをつけない人とつけている人と分けられているのが興味深い。
ほんとおまかせなんだなぁ

と、特に尖ったものも、面白いものもない。
(配信者じゃないので詳しく知らないですが、タグって後から編集できるようですね。
だからしばらくしてから編集されたものもあるかも。)






2018年4月22日日曜日

YouTube 生放送自動録画方法を考える

※この記事だけでは録画はできません。また、うまくいかないことがあると思います。

以前の記事でYouTuve Liveの検出もできたので録画もできるんじゃないかなぁと思い、
その手順を考えてみました。

生放送を検出する

ブラウザを立ち上げてその動画を表示

何かツールを使い録画する

終了を検出して録画を終了する、ブラウザの閉じる

最初に戻る。

という手順が考えられると思います。
ここでは、ブラウザを立ち上げて動画を表示するだけですが
コマンドプロンプトからキャプチャーソフトで録画・終了できるものは世の中には存在するので、そのキャプチャーソフトの使い方を参考にしてコードに反映させれば録画できるかもしれません。
今回はfirefoxを起動させていますが、他のブラウザだとウィンドウの最大化など制御項目が多いかも?


以下がコードとなります。以前の生放送の検出コードの改造です。
①は自分のAPIキー。②は生放送を監視したいチャンネルID
③にはキャプチャーソフトの録画開始処理。④はキャプチャーソフトの録画終了です。

livechkRec.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
$key = "①";
$channelId = "②";
 
$search_api_base = "https://www.googleapis.com/youtube/v3/search?part=snippet";
$search_api = $search_api_base . "&channelId=" . $channelId . "&type=video&eventType=live" . "&key=" . $key ;
 
//タイムゾーンを東京に設定
date_default_timezone_set('Asia/Tokyo');
echo  "起動開始時間". date('H:i:s') . PHP_EOL ;
 
loop:
//前半
$flag = 1;
while($flag)
{
 $search_contents = file_get_contents($search_api);
 $search_json = json_decode($search_contents,true);
 
 if(empty($search_json["items"]) == FALSE)
 {
  echo  date('H:i:s')  . "に放送開始!" . PHP_EOL ;;
  $videoId = $search_json["items"][0]["id"]["videoId"];
  $exestr = "Start firefox -new-window"." https://www.youtube.com/watch?v=" . $videoId;
  $ftolk =  popen($exestr,'r');
  pclose($ftolk);//ファイルポインタを閉じてるだけ
 
  //③ここに録画する処理を足す
  $flag = 0;//whileから抜ける
 }
 //60秒遅延
 usleep(60*1000000);
}
 
//後半
$flag = 1;
while($flag)
{
 $search_contents = file_get_contents($search_api);
 $search_json = json_decode($search_contents,true);
 
 if(empty($search_json["items"]) == TRUE)
 {
  echo  date('H:i:s')  . "に放送終了!" . PHP_EOL ;
  //④ここに録画を終了する処理を足す・ブラウザも閉じる!
  $flag = 0;//whileから抜ける
 }
 //60秒遅延
 usleep(60*1000000);
}
 
 
goto loop;
 
?>


注釈として
12行目と53行目がペアとなっています。処理が終了したら最初に戻るgoto処理。
26行目はファイルポインタを閉じているだけでブラウザを閉じるわけではありません。
PHPではアプリケーションを起動する関数はいろいろあるようですが、これが最適かはわかりません。とりあえず起動はできます。
24行目 引数に開きたいURLを付けるとfirefoxはそのURLを開く
42行目は生放送の終了条件のチェックです。60秒周期で監視しているので生放送1→生放送2も続けて同じ放送と判定されるから同じvideoIdであるか判定したほうがいいかも。

改善点・注意点として
firefoxの新しいウィンドウとして表示されますが、前回のブラウザの大きさに左右されてたりブックマークのサイドバーの状態が維持されています。
生放送でも広告が表示されることがありますが手動スキップはできません。
広告が1時間とか長かったから手も足も出ないです。
広告後に再生が手動の場合もある?
生放送後砂嵐画面で止まるなら音がでていないのでブラウザを終了する必要ないかも。
そもそもYouTubeが最近止まるってリロード必要の場合があるし。。。
60秒周期で処理していますがもっと短くしないと動作確認も大変かも
自分が家にいないときに動画が流れてうるさい場合もあるかも。
前回同様、限定公開も無理。

など考える点がたくさん。アプリの組み合わせで実現するだけだから仕方ないよね・・・
録画しっぱなし、生放送は一回だけの想定ならこれで使えないことはないと思う。
もう出先で通知を受け取ってリモートデスクトップの操作のが楽か?
あまり自分には生放送を録画するという必要に迫られていないのでこのぐらいでクローズ

MMOのチャットログを翻訳→読み上げる

MMO全盛期にできてればよかったのになぁ

8年以上前に海外のMMOを(英語がまったくもってできない)のにやっていたころ、
チャットログを翻訳できれば便利なのになぁと思ってなんとかできないかちょっと検討してみたのですが、
ゲーム側が記録続けているテキストファイルに対して最新の行を取得する方法がいまいちわからなかったのですよね。

時が立ちいろいろやってくうちに少し解決しました(C++ですけど)
対象のテキストファイルが更新されても一度EOFに到達している後は新しい行が取得できない
原因はEOFを一度見つけるとEOFを見つけたフラグが立つから。
それでfgetsで続きがよめなくなるので、このフラグをクリアする(ifs.clear()。)
このころは英語のMMOを既にやってないので翻訳は気にしなかったし、C++でAPI投げる方法も全然わからなかった。

今はPHPでYouTube Data APIを投げるのは慣れてきたので、できるじゃんということでやってみました。
PHPではifs.clear()みたいなのが見当たらないので
fseek($fp, ftell($fp))で確認した行まで再度シークすることによって
EOFのチェックを再び行うことによって実現しています。
この更新され続けるテキストを監視する方法がネットで検索しても出てこなかったから苦労した。
需要はありそうだし、他にエレガントのやり方があるのだろうか?

翻訳の部分はYouTube Data APIと同じノリで 「Google Cloud Translation API」を使います。
ただし、これは有料の機能です。
Google Play Consoleで300ドル分のクレジット期限付きであったのでこれを活用。(お試し用?)
20ドル/100万文字(全部マルチバイト文字だとたら200万バイト→1.9MB相当?)だそうですが10万文字で10ドルなど使った分だけ請求されるみたい。
Google Play Consoleの方でYouTube Data APIと同じように「Google Cloud Translation API」を有効にします。
既存のAPIキーがあればそれで「Google Cloud Translation API」が使用できます。
課金されるのでAPIキーが他人に漏れないように!


ついでにYouTubeの方もチャットも読み上げソフトで読めたらなぁと思い練習用に
翻訳後の結果を有名なソフト「棒読みちゃん」に読み上げさせます。
これは「棒読みちゃん」のフォルダの下にある「RemoteTalk.exe」にメッセージを投げればよいです。


以下、今回のコードの仕様
・PHPファイルと同じフォルダにあるtest.txtを対象とします。
・PHP実行後にtest.txtに追加された行を対象とします
・英語→日本語の翻訳を行います。(もともと日本語の場合もAPIへ投げているので課金されると思います。)
・翻訳結果を棒読みちゃんで読み上げを行います。
・ゲームチャットのログとして実行例として自分が昔やっていたMMO(サービス終了済み)を使用します
使用したゲームのログにあわせてメッセージ部分を取り出してください。
・自分の発言はロボット、それ以外は女性1の声質を使います

テスト用txtファイル例 これを上から5行目に1行ずつ足して保存すれば翻訳されたものが読み上げられます。
TEST.txt
1
2
3
4
2024/12/01 21:34:10 :>パーティー< :Let's attack from the back of the enemy
2024/12/01 21:39:21 :<パーティー> Okayu :用意ができたら
2024/12/01 21:39:27 :<パーティー> Okayu :When ready
2024/12/01 21:39:09 :>パーティー< :Then please tell me if you can



以下がコードとなります。①は自分のAPIキー。②は自分のフォルダ位置に合わせてください。
使用する前に棒読みちゃんを起動した状態で行ってください
ReadChat.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php
define("APIKEY","①");
 
//翻訳関数
function trns_us($text)
{
 $url = "https://www.googleapis.com/language/translate/v2?key=" . APIKEY . "&source=en&target=ja&q=".urlencode($text);
 $resjson = file_get_contents($url);
 $resjson = mb_convert_encoding($resjson, 'UTF-8', 'auto');
 $resarray = json_decode($resjson);
 $result = $resarray->data->translations[0]->translatedText;
// echo $result;
 return $result;
}
 
//RemoteTalkへのパス
$toolpath="C:\\Users\\HogeHoge\\Documents\\tool\\BouyomiChan\\RemoteTalk\\RemoteTalk.exe /T ";//←②
 
define("TESTFILE","./TEST.txt");
 
$fp = fopen(TESTFILE, "r");
fseek( $fp, 0, SEEK_END);//最後の行へ移動
 
while(1)
{
 while ( $line = fgets($fp) )
 {
//  echo $line;
 
  //フォーマットに合わせて分解する
   $data=explode(":",$line);
  $str = $data[1];
 
  $data1=explode(" ",$data[0]);
  if(count($data1) == 5)
  {
   $name = $data1[3];
   $voice = " -1 -1 -1 1";//女性1
  }
  else
  {
   $name = "自分";
   $voice = " -1 -1 -1 6";//ロボット
  }
 
  //echo $name . PHP_EOL;//キャラ
   //echo $str . PHP_EOL;//テキストデータ
   
  $str = trns_us($str);//翻訳
   
  //棒読みちゃんに飛ばす
  $tmp_cmd = $toolpath ."\"". $str."\"".$voice;
  $ftolk =  popen($tmp_cmd,'r');
  pclose($ftolk);
 
  //配列のクリア
  unset($data);
  unset($data1);
 }
 usleep(1*1000000);//1秒待ち
 fseek($fp, ftell($fp));//再チェック
}
 fclose($fp);
 
?>



注釈として
//echoはデバッグ用です。
17行目は自分のフォルダに合わせてください。\\は2つずつ書くようにしてください。
31行から44行目は自分のゲームのフォーマットに合わせ。声の割り当ても自分の趣味で。
7行目。URLに翻訳した文とAPIキー。翻訳元言語、翻訳先言語を指定して投げると
JSONでレスポンスが返ってくるので結果を取り出すだけ
日本語だろうとかまわずAPIに投げます。
49行目 ここコメントアウトで翻訳なしに。
52行目。スペースが入ったりするとアレなので””で囲む。
53行目と54行目。PHPからのアプリ実行はいくつも方法があるのでよくわからないけど、
これでよさそう。
63行目。ここには到達しません。


改善点として
対象ファイルはドラッグとかで指定できるようにしたほうがいい
<ささやき>や<シャウト>で音量を変えたり、特定の人に特定の声質を当てるなど自分で好きなようにコーディングして調整してみたください。
エラーチャックはしてません。いつも通り動けばいいで雑です。
ファイルの行が減ったりすると暴走しそう。
YouTubeのチャットも同じように投げたり翻訳できたり自分用にカスタマイズできますね。
正規表現でチャットログのメッセージを分解した方がエレガントかも。


昔できなかったことができるってのは成長です!
次の機会まで 「Google Cloud Translation API」は無効にして封印!

2018年4月16日月曜日

目次:YouTube Data APIで遊ぶ

このブログ内のYouTube Data API関係の記事についての目次です。

1:PHPの開発環境
APIを使うために開発環境についての説明

2:YouTube Data APIの使い方 
APIキーについてとAPIの簡単な動作確認
API使用量制限について説明

3:YouTube生放送の検出 
コマンドプロンプトで生放送の開始を監視してビープ音を鳴らすというプログラムについての説明

4:YouTube コメントの全取得
動画のコメント欄を一括で取得するプログラムについての説明

5:OAuth2.0での認証と登録チャンネルリストの出力 
認証の方法
アクセストークンについての説明
自分自身の登録チャンネルをリスト出力

6:YouTube チャットの取得
生放送でのチャットメッセージ情報を取得

7:YouTube 生放送中の評価芸
API使って評価数や視聴者数を取得

8: YouTube生放送で評価数をリアルタイムでグラフ表示する 
PHPで難しそうなのでJavaScriptを使ってリアルタイムで変化するグラフ表示

9: YouTube 生放送自動録画方法を考える 
とりあえず考えてみる。対象の動画の表示だけはできたが・・・

10:YouTubeチャットで視聴者参加のためのツールを考える 
上位のバーチャルユーチューバーのチャット欄は早くて読みにくいので、
たぶんこんな感じで対象者を募って決めて。対象者だけ表示すると予想。

11:YouTubeの生放送のチャットを使ってアンケートを行う 
チャットのコメントでアンケートを実装してみる。

12:YouTubeの生放送のチャットをPHPから書き込む
チャットのコメントにPHPから周期的に書き込むサンプル。

以上。

YouTube生放送で評価数をリアルタイムでグラフ表示する

いままでPHPでがんばってたんですが。。。
おかしいよな、PHPでやり方を発信するはずがJavaScriptを使用なんて。


グラフ表示には 調べた結果Highchartsというのがよさそう

でもJavaScriptで書く必要がある

PHPのデータで保存したファイルをJavaScriptで逐次読もうと考えたけど難しそう

PHPあきらめて JavaScriptでやり始める

Firefoxにできたhtmlファイルを突っ込んで確かめていたけど、途中でChromeを右クリックの「検証」でブレークとかエラー表示が見えることに気付く

グラフの見栄え・気になる所をいろいろいじったらすごい時間をかかるのである程度妥協する

そして、出来上がったのがこれ

 HighchartsとJavaScriptを無勉から、で作り上げました。
Highchartsのこちらをベースにしています。

仕様は
 ・20点のデータ
 ・2秒間隔で取得
 ・高評価と低評価。チャンネル登録者数を表示
 ・最新値が見えるようにする



右が最新のデータで、左が過去データで流れていきます。

これがコードです。このhtmlをChromeやFirefoxに放り込んでみてください。古いIEでは動きません。


LivedataChart.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
<!DOCTYPE html>
<html lang = "ja">
  <head>
    <meta charset = "utf-8">
    <title>Live data</title>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/exporting.js"></script>
  
<div id="container" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
  </head>
  <body>
   
<script>
var input_y0=0,input_y1=0,input_y2=0;
var key = "①";
var videoId = "②";//今回はvideoIdを入力
var channelId="";//こっちは空欄でOK
 
    function requestvideo() {
      return $.ajax({
                type: 'GET',
                url: 'https://www.googleapis.com/youtube/v3/videos?part=statistics,snippet,liveStreamingDetails&id=' + videoId + '&key=' + key,
                dataType: 'json',
                async: false,//同期処理にする
                error: function() {
                    //error func
                    $('#yt-list').append('<li>Youtubeのデータが取得できませんでした。<\/li>');
                }
      }).done(function (jsondataYt) {
   channelId = jsondataYt.items[0].snippet.channelId;
         input_y0 = parseFloat(jsondataYt.items[0].statistics.likeCount);
         input_y1 = parseFloat(jsondataYt.items[0].statistics.dislikeCount);
      });
    }
 
 
    function requestchannel() {
      return $.ajax({
                type: 'GET',
                url: 'https://www.googleapis.com/youtube/v3/channels?part=statistics&id=' + channelId + '&key=' + key,
                dataType: 'json',
                async: false,//同期処理にする
                error: function() {
                    //error func
                    $('#yt-list').append('<li>Youtubeのデータが取得できませんでした。<\/li>');
                }
      }).done(function (jsondataYt) {
         input_y2 = parseInt(jsondataYt.items[0].statistics.subscriberCount);
      });
    }
 
   
 
 
 
Highcharts.setOptions({
  global: {
    useUTC: false
  },
    lang: {
        numericSymbols: null//軸のkキロ表示をやめる
    }
});
 
Highcharts.chart('container', {
  chart: {
   type: 'spline',
 
    animation: Highcharts.svg, // don't animate in old IE
    marginRight: 100,//右の軸が隠れるので適切な値にする
    events: {
      load: function () {
 
        // set up the updating of the chart each second
        var series0 = this.series[0];
        var series1 = this.series[1];
        var series2 = this.series[2];
 
        setInterval(function () {
          var x = (new Date()).getTime(); // current time
          requestvideo();//YouTubeAPI ビデオ側
          requestchannel();///YouTubeAPI チャンネル側
 
          series0.addPoint([x, input_y0], true, true);
          series1.addPoint([x, input_y1], true, true);
          series2.addPoint([x, input_y2], true, true);
          series0.update({name: '高評価:'+input_y0});
          series1.update({name: '低評価:'+input_y1});
          series2.update({name: '登録者数:'+input_y2});
       }, 2000);//2秒のインターバルタイマ
      }
    }
  },
  title: {
    text: 'Live data'
  },
  xAxis: {
    type: 'datetime',
    tickPixelInterval: 150
  },
  yAxis: [
      { // 1つ目のy軸設定
          title: {
              text: '評価'
          },
          plotLines: [{
              value: 0,
              width: 1,
              color: '#FF8080'
       }],
       allowDecimals: false //軸を整数表示
      },
      { // 2つ目のy軸設定
          title: {
              text: '登録者数'
          },
       allowDecimals: false, //軸を整数表示
          opposite: true // trueの場合グラフの右側にy軸を配置する
      }
  ],
  tooltip: {
    formatter: function () {
      return '<b>' + this.series.name + '</b><br/>' +
        Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', this.x) + '<br/>' + this.y;
    }
  },
  legend: {
    enabled: true //凡例表示
  },
  exporting: {
    enabled: false
  },
  series: [{
    name: '高評価',
    yAxis: 0,
    data: (function () {
      var data = [],
        time = (new Date()).getTime(),
        i;
 
      for (i = -19; i <= 0; i += 1) {
        data.push({
          x: time + i * 1000,
          y: null
        });
      }
      return data;
    }())
  },
  {
    name: '低評価',
    yAxis: 0,
    data: (function () {
      var data = [],
        time = (new Date()).getTime(),
        i;
 
      for (i = -19; i <= 0; i += 1) {
        data.push({
          x: time + i * 1000,
          y: null
        });
      }
      return data;
    }())
   },
  {
    name: '登録者数',
    yAxis: 1,
    data: (function () {
      var data = [],
        time = (new Date()).getTime(),
        i;
 
      for (i = -19; i <= 0; i += 1) {
        data.push({
          x: time + i * 1000,
          y: null
        });
      }
      return data;
    }())
   }
    
   ]
});</script>
 
 
  </body>
</html>

注釈として
なんか上のコードの色が変ですがあきらめました。
Highchartsは、ダウンロードしても使えるみたいですがしなくてもできるらしいです。今回はダウンロードなしで使用しています。
16と17行目はPHPと使い方は同様です。APIキーと動画IDいれてください。
APIの使いかたはPHPと同じようなものです。URL作って投げて、レスポンスを使用する形は変わりません。
25行目と43行目については、PHPと違いJavascriptだとレスポンスが返るまでまたず処理が進んでしまう、雑な対策としてこの行追加でレスポンスが返るまで待ち状態となります。これでPHPと同じ感じに動作します。
62行目 Y軸が1000を1kとかの表示するのを禁止します。
71行目 第2のY軸を右に置こうとしたら表示されないので苦労しました。原因は画面の外に表示されていました。この値を増加したら解決。これで1時間は無駄にしました。
80行目は2秒周期インターバル動作させています。
85行目あたりaddPointのtrueとかすることによってシフト動作してくれます
88行目 汎用に最新値表示しています。最新の値をプロット上に出したかったのですがdataの要素の最後だけlabel表示をenableにしてもシフト動作すると前の要素に移動していってしまうのでaddPointを使わずfor文なりで更新するのがよさそう?メンドクサイのであきらめました。
112行目と118行目でY軸が整数表示
119行目で二つ目のY軸を右側に置きます
136行目でどちらのY軸に表示するか決まります
142行目等のi=-19でデータの点数を20点してます。-199にすれば200点になります。


なんとかやりきった。疲れた。

2018年4月15日日曜日

評価を実際見てみる

せっかく生放送の評価を取得できるようになったので実際とってグラフにしてみました。

一つ目。しばらく前の某バーチャルユーチューバーの麻雀配信の最後の方だけです。
たしか2戦目で配信者がビリになり、罰ゲームのルイズコピペ改変で場が温まっている後です。


Aの低評価増加は、配信者が1位をとった所です。
Bの低評価減少、高評価増加は罰ゲームをやりきった謎の感動
Cのピークは、 配信者実質3連続1位とか言いだしたイキリ芸のところです。少し前の高評価増はゲストのために評価戻しといてねの発言があったからです。
高評価と低評価が逆の形をしているので、コミュニケーションの一環みたいですね。

YouTube 生放送中の評価芸

生放送中をみるようになって思ったのですけど、
例えばチャンネル登録数がもう少しで5000人になるって時にわざと解除したり、
一発芸すると低評価が入ってすぐ戻ったりなど
評価の上下で視聴者とコミュニケーションするのだなと思いました。

ということで、生放送中の評価を取得するプログラムを書いてみました。
仕様は
・最初にタイトル等を表示する
・時間,視聴者数,再生数,高評価,低評価,チャンネル登録数を記録する
・10秒間隔で取得する
・終了条件はなし
です。
特に今回は追加のテクニックはありません。  ①にはAPIキー、②には今回は生放送中の動画のidを入れます。
https://www.youtube.com/watch?v=xxxxxxxxxxx
のxxxxxxxxxxxの文字列のことです。



livedata.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?php
$key = "①";
$videoId = "②";//今回はvideoIdを入力
 
// 処理開始日時
//タイムゾーンを東京に設定
date_default_timezone_set('Asia/Tokyo');
echo  "処理開始日時". date('Y-m-d H:i:s') . PHP_EOL;
 
//生放送の情報の取得して表示
$video_url_base = "https://www.googleapis.com/youtube/v3/videos?part=snippet,liveStreamingDetails";
$video_url = $video_url_base . "&id=" . $videoId . "&key=" . $key;
$video_contents = file_get_contents($video_url);
$video_json = json_decode($video_contents,true);
 
//配信タイトル(トピック)取得
$title = $video_json["items"][0]["snippet"]["title"];
//配信開始時刻取得
$StartTime = $video_json["items"][0]["liveStreamingDetails"]["actualStartTime"];
//チャンネル名取得
$channel_name = $video_json["items"][0]["snippet"]["channelTitle"];
//説明文取得
$description = $video_json["items"][0]["snippet"]["description"];
 
echo "タイトル:" . $title . PHP_EOL ;
echo "放送開始時間:" . date("Y-m-d H:i:s",strtotime($StartTime)) . PHP_EOL ;//タイムゾーンを東京に変更している
echo "チャンネル名:" . $channel_name . PHP_EOL ;
echo "概要欄:" . $description . PHP_EOL . PHP_EOL;
 
//チャンネルIDを取得
$channelId = $video_json["items"][0]["snippet"]["channelId"];
 
 
 
echo "10秒ごとにサンプリングする" . PHP_EOL;
echo "時間,視聴者数,再生数,高評価,低評価,チャンネル登録数" . PHP_EOL;
 
$video_url_base = "https://www.googleapis.com/youtube/v3/videos?part=statistics,liveStreamingDetails";
$video_url = $video_url_base . "&id=" . $videoId . "&key=" . $key;
 
$channel_url_base = "https://www.googleapis.com/youtube/v3/channels?part=statistics";
$channel_url = $channel_url_base . "&id=" . $channelId . "&key=" . $key;
 
while(true)
{
 //視聴者数 再生数 高評価 低評価
 $video_contents = file_get_contents($video_url);//動画側へのAPIを叩く
 $video_json = json_decode($video_contents,true);
 
 $viewCount = $video_json["items"][0]["statistics"]["viewCount"];
 $likeCount = $video_json["items"][0]["statistics"]["likeCount"];
 $dislikeCount = $video_json["items"][0]["statistics"]["dislikeCount"];
 $viewers = $video_json["items"][0]["liveStreamingDetails"]["concurrentViewers"];
 
 //チャンネル登録数
 $channel_contents = file_get_contents($channel_url);//チャンネル側へのAPIを叩く
 $channel_json = json_decode($channel_contents,true);
 $subscriberCount = $channel_json["items"][0]["statistics"]["subscriberCount"];
 
 //表示
 echo date('Y-m-d H:i:s')  . ",";
 echo $viewers . ",";
 echo $viewCount . ",";
 echo $likeCount ",";
 echo $dislikeCount . ",";
 echo $subscriberCount . PHP_EOL;
 
 //10 秒待つ
 usleep(10*1000000);
}
?>


注釈としては
YouTubeからのレスポンスのタイムゾーンが違うので 7行目と26行目で日本時間に変換しています。
61行目から66行目は取得した情報をカンマ区切りで出力しています。



備考として
・生放送中にリアルタイムでグラフがだせたらもっといいかも。PHPだけでは無理っぽいけど。あとチャットの勢いとかだせたらいいかなぁ。
・生放送中のアーカイブをそのままアップするとどうも生放送中の再生数が引き継がれる感じでした。
・低評価と高評価や再生数が約12分毎で更新することもあるし、もっと短いこともあってよくわからない。YouTubeのアルゴリズムの問題?
・取得の間隔は評価の変化が激しい時に3秒とか短めにできたら評価芸がわかっていいかもしれない。今回は10秒としましたがもっと短い周期でも制限的に大丈夫かも。
・長時間取得しっぱなしだと1日の使用量の制限を超える可能性があるので注意する。
・配信者は生放送中の上下が後からみることができるのだろうか?
・コードでは現在時刻で表示しているがliveStreamingDetailsのactualStartTimeでアーカイブの再生時刻と合わせられるかもしれない

2018年4月13日金曜日

YouTube チャットの取得

今回はYouTuve Liveのチャットの取得に挑戦します。

ということで、チャットが取得できるようになれば
特定の人を見えなくしたいとか、
特定の人だけピックアップするとか、
特定のキーワードで何かが加速したり、
スーパーチャットで何かしたり、
他のアプリケーションとの連携したり
チャットを保存したい
読み上げソフトで読み上げたり
翻訳したり
など、いろいろな目的がかなうようになると思います。

今回の仕様はこんな感じです。
・名前とテキストのみを表示
・コマンドプロンプトで表示する
・”www”(半角)を含むテキストを”NGNGNGNG”に置き換える
です。

使用しているAPIについてはvideoLiveChatMessagesを参照。
①にはAPIキー、②には今回は生放送中の動画のidを入れます。
https://www.youtube.com/watch?v=xxxxxxxxxxx
のxxxxxxxxxxxの文字列のことです。


以下がコードになります。 ※少ししか動作確認していません。
 追記2018/4/29 下の方に処理タイミングを変更したコードを載せました。


chat.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
$key = "①";
$videoId = "②";
 
//ChatIdの取得
$search_api="https://www.googleapis.com/youtube/v3/videos?part=liveStreamingDetails&id=" . $videoId ."&key=" . $key;
$search_contents = file_get_contents($search_api);
$search_json = json_decode($search_contents,true);
$ChatId = $search_json["items"][0]["liveStreamingDetails"]["activeLiveChatId"];
 
//ChatIdからliveChatにアクセス用のURL
$search_api="https://www.googleapis.com/youtube/v3/liveChat/messages?part=snippet,authorDetails&liveChatId=" . $ChatId ."&key=" . $key;
 
$next = "";
$url = $search_api;
 
while(1)
{
 if($next)
 {
  $url = $search_api . "&pageToken=" . $next;
 }
  
 $search_contents = file_get_contents($url);
 $search_json = json_decode($search_contents,true);
 
 foreach($search_json["items"] as $k => $data)
 {
  echo $data["authorDetails"]["displayName"];
  echo PHP_EOL;
  echo "\t";
  $text = $data["snippet"]["textMessageDetails"]["messageText"];
  if(strpos($text,"www") === false)
  {
   echo $text;
  }
  else
  {
   echo "NGNGNGNG";
  }
  echo PHP_EOL;
 }
 
 $next = empty($search_json["nextPageToken"])? 0 : $search_json["nextPageToken"];
 //pollingIntervalMillis + 500ms加算時間毎に読み込む
 $delay = $search_json["pollingIntervalMillis"]*1000 + 500*1000;
 usleep($delay);
 
}
 
?>



注釈として
6行目はpart=liveStreamingDetailsとして、”ChatId”を取得しています。
12行目でわかるように今回も続きを読み出すために&pageToken=を使用しています。
ただし、46行目にあるように"pollingIntervalMillis"より早くAPIを投げるとエラーとなるのでそれより500ms多めに遅延させています。
29行目から411行目を変更すればいろいろな機能を持たせられると思います。
コマンドプロンプトで>chatlog.txtやファイル書き込みで保存もできます。

結果はこんな感じです。
コマンドプロンプトなのでいまいちな見た目ですが・・・残像でるし

改善点として
・絵文字も使えないのでやっぱりhtmlなどでやるべきかなぁと。でも更新処理どうしていいかわからない。。。
・チャットが激しすぎるとどうなるのだろうという疑問。1回で取得する数のデフォルトは500ですがそれ以上の激しさでも追いつくか。
・pollingIntervalMillisは勢いで変化します。激しいほど長い感じです。
自分が見たのは4秒から9秒とかでした。
取得したメッセージを一気に表示しているのでpollingIntervalMillis/pageInfo.resultsPerPage
との間隔で少しずつ出力した方が自然な感じがします。
・生放送の終了判定がありませんので自分で強制終了するか確認処理が必要かと思います。放送自体が終了してもチャットが生きてたりで動作確認が必要かも。upcoming中はとりあえず取得ができる。

 -------------------------------------------------------------------------------------
追記2018/4/28
 配列でバーチャルYoutuber300人程度の発言のみを抽出コードを作って生放送見てたんだけど少し抜けがある。抽出処理の時間分待ち時間削ってないし、500ms分固定でプラスしてるのもイケてないのかも。
某所のコメントログでは比べるとどちらも抜けがあるしチャットは結構きついのかなぁ
公式のサンプルをもっと見た方がよかったか。
追記2018/4/29
 pollingIntervalMillisに対してフィルタ処理はそんな重くないことがわかったんで、とりあえず500ms固定を削除して待ち方を変更。
コードは以下


chat2.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
$key = "①";
$videoId = "②";
 
//ChatIdの取得
$search_api="https://www.googleapis.com/youtube/v3/videos?part=liveStreamingDetails&id=" . $videoId ."&key=" . $key;
$search_contents = file_get_contents($search_api);
$search_json = json_decode($search_contents,true);
$ChatId = $search_json["items"][0]["liveStreamingDetails"]["activeLiveChatId"];
 
//ChatIdからliveChatにアクセス用のURL
$search_api="https://www.googleapis.com/youtube/v3/liveChat/messages?part=snippet,authorDetails&liveChatId=" . $ChatId ."&key=" . $key;
 
$next = "";
$url = $search_api;
 
while(1)
{
 if($next)
 {
  $url = $search_api . "&pageToken=" . $next;
 }
  
 $search_contents = file_get_contents($url);
 $search_json = json_decode($search_contents,true);
 //時間測定開始
 $time_start = microtime(true);
 
 foreach($search_json["items"] as $k => $data)
 {
  echo $data["authorDetails"]["displayName"];
  echo PHP_EOL;
  echo "\t";
  $text = $data["snippet"]["textMessageDetails"]["messageText"];
  if(strpos($text,"www") === false)
  {
   echo $text;
  }
  else
  {
   echo "NGNGNGNG";
  }
  echo PHP_EOL;
 }
 
 $next = empty($search_json["nextPageToken"])? 0 : $search_json["nextPageToken"];
 //pollingIntervalMillis msの整数型を実数にしている
 $delay = $search_json["pollingIntervalMillis"]/1000.0;
 do{
  $time = microtime(true) - $time_start;
  usleep(100);//0.1ms待ち
 }while($time < $delay);
}
 
?>


注釈として
 51行目の待ちがないとCPU負荷が高いので挿入しています。

これで抜けがいくらか減ると思います。