ニコニコ動画のHTML5プレイヤーにニワン語が来た件(嘘)

ニコニコ動画のHTML5プレイヤーにニワン語が来た件(嘘)

えねこです、あけましておめでとうございます。

2020年もよろしくお願いします。

長期連休の時ぐらいしか活動してないのですが何かそれっぽいことしたいなと思いましてこの記事を書いています。

はじめに

「動画上にただコメントを出すだけって誰が決めた?」

という発想から構想を練ったものになります。
コメントだけは飽きた
なにいってんだこいつ?って思った方、正解です。俺もなにいってんだろって感じです。

何かコメントアートって感じで限られた枠やルールの中で表現するのってもちろん楽しいと思うんですけど
現状の中で色々やるのも飽きてしまったんですよね。
やっぱりモチベーション低下って変化が無いことに対して起きるのかなと思っています。
なのでそれを払拭したく自分なりに解決をはかってみました。

どんな内容か

まずは下の2つの動画を見て頂ければと思います。


お気づきの通りニコニコ動画のHTML5プレイヤーに通常サイズでは表示出来ないコメントやアニメーションがついています。
フラッシュプレイヤー時代のニワン語っぽくもありますね。

さてさてこれはどういうことなんだろうという所だと思いますので次から解説を入れていきます。

先にネタバレ

まぁ要するに、Chromeの拡張機能として疑似的なニワン語をつくったった です。

  • これは実際に実装されたものではありません
  • Chrome拡張機能を用いてコメント欄の内容とhtml(+css, javascript)を組み合わせて描画しています
  • 公開の予定は今の所無し(完全にお試しで作っていたので)
  • 投稿者コメントの画面上のみ現時点は動きます

特定の構文を入力すると下の動画のようにアニメーションやエフェクトをつけることが出来ます。

もう少し詳しく

言語

javascript(typescriptベースでトランスパイラル)

フレームワーク

無いよ

ライブラリ

役割の説明

javascript(typescript)の役割

基本的にはjavascriptが色々頑張ってくれています。

  • コメント内容の監視
  • コメント内容の解析
  • コメント内容に応じた描画

ライブラリの役割

ネイティブなjavascriptでもある程度似たようなことは可能なのですが、いちいちそれを実装していたらとてもじゃないですが時間が足りません。
ライブラリはその辺の面倒くさい構文とか処理を必要なパラメータを渡してあげるだけでそれっぽく処理してくれる便利処理をまとめたものです。

今回は文字のアニメーションはライブラリに任せています。

ブラウザの役割

まぁそりゃこれがないと始まらないですよね。

処理の話

アルゴリズム

アルゴリズム的な話ですが、ざっくり書くと下記の流れです。
①一定時間ごとに投稿者コメント欄の内容をチェック
②表示時間、コマンド、コメント内容をリストに持つ
③リストの中の表示時間と、プレイヤーの表示時間を比較する
④プレイヤーの表示時間と合致するリスト中のコメントがあれば内容を解析する
⑤解析した内容を画面に描画する

もう少し詳しく

なんとなく伝わりますかね?
肝となるのはコメント内容の取得と解析、インターバル、描画ですね(全部)
この辺で少しコードっぽいの載せてみます。
※コードは適当にお酒飲みながら書いて3日ぐらいでそれっぽい感じにしたのでだいぶひどい
※一部が非同期処理してるので処理が一定の順番で流れているという感じではないので雰囲気だけ見てもらえればと思います

①一定時間ごとに投稿者コメント欄の内容をチェック

// 時間のチェック開始
const test = setInterval(checkNowPlayMovieTime, 100);

let alreadyShowComment: boolean = false;
let beforePlayTime: Date = new Date(2020, 4, 1, 0, 0, 0);
function checkNowPlayMovieTime(): void {
    // 表示されている時間を取得する
    // console.log($(".PlayTimeFormatter.PlayerPlayTime-playtime").text());
    commentDataLoading();

    // プレイヤーに表示している時刻
    let date1: Date = new Date(2020, 4, 1, 0, Number($('.PlayTimeFormatter.PlayerPlayTime-playtime').text().split(':')[0]), Number($('.PlayTimeFormatter.PlayerPlayTime-playtime').text().split(':')[1]));
    let date2: Date = beforePlayTime;
    for (let i = 0; i < commentDataList.length; i++) {
        // 時間に合致するコメントがあったら処理する
        // STOPや検索のタイミングによっては既に貼り付けたコメントを再度はろうとするのでそれを除外する
        // 同一時間は処理をしない
        if ((date1.getTime() === date2.getTime()) && alreadyShowComment) {
            return;
        } else {
            alreadyShowComment = false;
        }

        // beforePlayTimeより値が小さければ巻き戻したと判断して表示している内容をすべて消す
        if (date1 < date2) {
            beforePlayTime = date1;
            $('.mainPosition').remove();
            // alreadyShowComment = false;
        } else {
            beforePlayTime = date1;
        }
        if ($('.PlayTimeFormatter.PlayerPlayTime-playtime').text() === commentDataList[i].timeSpan.split('.')[0]) {
            sleep(commentDataList[i].timeSpan.split('.')[1], commentDataList[i])
                .then(() => {
                    // console.log("TEST");
                })
                .catch((error) => {
                    console.log(error);
                });
        }
    }
}

メインはこの関数をずっとループしています。
1行目のsetIntervalで0.1秒ごとにチェックをしています。

②表示時間、コマンド、コメント内容をリストに持つ

/**
 * コメントデータを読み込む
 */
// グローバルなリストを持つ
let commentDataList: any = new Array();
function commentDataLoading(): void {
    // console.log($(".DataGrid-TableRow.CommentPanelDataGrid-TableRow"));
    commentDataList = new Array();
    let tempCommentData: any;
    // 解釈する
    for (let i = 0; i < $('.DataGrid-TableRow.CommentPanelDataGrid-TableRow').length; i++) {
        tempCommentData = $('.DataGrid-TableRow.CommentPanelDataGrid-TableRow')[i];

        if (tempCommentData.innerText.split('\n').length === 4) {
            // リスト生成
            commentDataList.push(new commentData(
                tempCommentData.innerText.split('\n')[0],
                tempCommentData.innerText.split('\n')[1],
                tempCommentData.innerText.split('\n')[2])
            );
        } else {
            // リスト生成
            commentDataList.push(new commentData(
                tempCommentData.innerText.split('\n')[0],
                '',
                tempCommentData.innerText.split('\n')[1])
            );
        }
    }
}

/**
 * Comment Dataクラス
 */
// tslint:disable-next-line:class-name
class commentData {
    timeSpan: any;
    command: string;
    comment: string;

    constructor(timeSpan: any, command: string, comment: string) {
        this.timeSpan = timeSpan;
        this.command = command;
        this.comment = comment;
    }
}

コメントの情報はcommentDataクラスを用意してリストとして扱っています。

③リストの中の表示時間と、プレイヤーの表示時間を比較する

/**
 * Creates comment layer
 * @param comment
 */
function updateComment(commentData: commentData): void {
    // コメント内容を解析して動画上のコメント内容を変更する
    analysisCommentData(commentData);
}

/* 小数点待機処理 */
const sleep = (waitTimes: any, commentData: any) => {
    return new Promise(resolve => {
        setTimeout(() => {
            // 遅延実行
            resolve(updateComment(commentData));
        }, waitTimes);
    });
};

コメントの投稿時間とリストの中の表示時間(00:00の形式)が合致していたら小数点の時間分sleepさせてからコメント内容の解析処理に入る。

④プレイヤーの表示時間と合致するリスト中のコメントがあれば内容を解析する

/**
 * コメント内容を解析して動作、クラス、レイヤーなどを決定する
 * @param {string} commentData
 */
function analysisCommentData(commentData: commentData): void {

    // method
    // ;区切りで複数の内容を受け付ける
    let loopCount: number = commentData.comment.split(';').length;
    for (let i = 0; i < loopCount; i++) {
        let splitedComment: string = commentData.comment.split(';')[i];
        try {
            switch (methodType(splitedComment)) {
                case 'create':
                    // コメントの内容に応じてhtml要素, css要素を生成する
                    let createData: any = createLayer(commentData, i);
                    // $(".cssanimation.sequence.rotateXIn").remove();
                    $('.VideoSymbolContainer').prepend(createData);
                    // $(".VideoSymbolContainer").prepend("<h1 class=\"cssanimation sequence rotateXIn\">" + commentData.comment + "</h1>");
                    break;
                case 'delete':
                    $('.mainPosition').remove();
                    break;
                case 'update':
                    break;
                case 'multi':
                    setLayer(commentData);
                    break;
                case 'other':
                    createData = showLogo(commentData);
                    $('.VideoSymbolContainer').prepend(createData);
                case 'createMotion':
                    createData = createMotion(commentData, i);
                    $('.VideoSymbolContainer').prepend(createData);
                    break;
                case 'setStyle':
                    setStyle(commentData, i);
                    break;
            }
        } catch {

        } finally {
        }
    }
    alreadyShowComment = true;
}

/**
 * 正規表現でメソッドタイプを取得する
 *
 * @param {string} splitedComment
 * @returns {string} メソッドタイプ
 */
function methodType(splitedComment: string): string {
    let mystr: string = splitedComment;
    // メソッドタイプを正規表現で抽出する
    let myregex: any = mystr.match('setStyle(.*?)|createMotion(.*?)|create(.*?)|delete(.*?)|update(.*?)|multi(.*?)|other(.*?)');

    // console.log(myregex[0]);
    return myregex[0];
}

構文は基本的に下記の内容。
create("本文", "カラーコード", "左上からのtopの座標", "左上からのleftの座標", "フォントサイズ");
delete("all");
multi("fadeout");
createMotion("class名","本文","カラーコード", "左上からのtopの座標", "左上からのleftの座標", "フォントサイズ");
など。メソッド名とかは適当なのでちょっとなんとかしたい・・。

あとでなんとかしたいロジック郡
todo: 関数化とか諸々


function commentDataParse(comment: string): any {
    // メソッド部を削除する
    comment = comment.replace('createMotion(', '');
    comment = comment.replace('create(', '');
    comment = comment.replace('delete(', '');
    comment = comment.replace('update(', '');
    comment = comment.replace('multi(', '');
    comment = comment.replace('setStyle(', '');
    // 末尾の ); を消す
    comment = comment.replace(')', '');

    comment = comment.replace(/, \"/g, ',\"');
    let mystr: any = comment.split(',');
    // 各項目のダブルクォートを除去
    for (let i = 0; i < mystr.length; i++) {
        mystr[i] = mystr[i].replace(/\"(.*?)\"/g, '$1');
    }
    // let myregex: any = new RegExp("((.*?)\);");
    // let result: any = mystr.match(myregex);

    // console.log(result[1]);

    return mystr;
}

function setStyle(commentData: commentData, splitNo: number): void {
    // メソッド部を削除する
    let tempCommentData: string = commentData.comment;
    tempCommentData = tempCommentData.replace('createMotion(', '');
    tempCommentData = tempCommentData.replace('create(', '');
    tempCommentData = tempCommentData.replace('delete(', '');
    tempCommentData = tempCommentData.replace('update(', '');
    tempCommentData = tempCommentData.replace('multi(', '');
    tempCommentData = tempCommentData.replace('setStyle(', '');

    // 末尾の ) を消す
    tempCommentData = tempCommentData.replace(')', '');

    tempCommentData = tempCommentData.replace(/, \"/g, ',\"');
    tempCommentData = tempCommentData.replace(/;/g, '');
    let mystr: any = tempCommentData.split(',');
    // 各項目のダブルクォートを除去
    for (let i = 0; i < mystr.length; i++) {
        mystr[i] = mystr[i].replace(/\"(.*?)\"/g, '$1');
    }

    $('.mainPosition').addClass(mystr[0]);
}

⑤解析した内容を画面に描画する

function createMotion(commentData: commentData, splitNo: number): string {
    // 設定内容をパース
    let tempCommentData = commentData.comment.split(';')[splitNo];
    let commentDetails: any = commentDataParse(tempCommentData);
    // 設定内容をパース
    tempCommentData = commentData.comment.split(';')[splitNo];
    commentDetails = commentDataParse(tempCommentData);
    let styles: any = 'top: ' + commentDetails[3] + 'px; left: ' + commentDetails[4] + 'px; color: ' + commentDetails[2] + '; font-size:' + commentDetails[5] + 'px"';
    let tempCreateData: any = '<h1 style="' + styles + '" class="' + commentDetails[0] + ' mainPosition' + '">' + commentDetails[1] + '</h1>';

    return tempCreateData;
}

/**
 * 実要素の設定を生成するメソッド
 *
 * @param {commentData} commentData
 * @returns
 */
function createLayer(commentData: commentData, splitNo: number): string {
    // 設定内容をパース
    let tempCommentData = commentData.comment.split(';')[splitNo];
    let commentDetails: any = commentDataParse(tempCommentData);
    let styles: any = 'top: ' + commentDetails[2] + 'px; left: ' + commentDetails[3] + 'px; color: ' + commentDetails[1] + '; font-size:' + commentDetails[4] + 'px"';
    let tempCreateData: any = '<h1 style="' + styles + '" class="cssanimation sequence rotateXIn ' + 'mainPosition' + '">' + commentDetails[0] + '</h1>';

    return tempCreateData;
}

これで画面に表示

$('.VideoSymbolContainer').prepend(createData);

はい、ほとんどの方はわかっていると思いますが動画上にHTMLの要素として文字を出したりアニメーションをさせていたのでした。

補足

アニメーションについて

アニメーションはライブラリを使用しています。
CSS Animation Library for Developers and Ninjas – cssanimation.io
例えばこちら

See the Pen
Animate.css
by Levi Neuland (@levineuland)
on CodePen.

要素を生成する時に特定のclass名を与えるとその内容のアニメーションが走るようにしていました。
なので既存のニワン語のように複雑なコードを書かなくてもそれっぽい感じになっていたのでした。
アニメーションは今回の肝だったのですがこの方法で実現出来たのは良かったなと思っています。

おわりに

ということで年末から年明けにかけてちょっとやってみよう、からはじめてそれっぽい形になったのはよかったなーと思いました。
この内容は拡張機能を読み込めれば別の人でも同じように見えるのですが、全ユーザーというわけにはいかないので課題がありますね。

でもコメントに親しいなにかを触るというところで面白かったです。
地味に皆さんの反応も新鮮でよかったw

コメントという枠組みにとらわれずに今後も色々挑戦出来たらなと思います。
乱文ですが以上です、ここまで読んで頂きありがとうございましたm( )m

構想:1日ぐらい
実装:2日ぐらい

p.s. 結局コメントした内容を解析してるから俺はまだコメントの枠組みに囚われていることに気づきました。 -end-

制作講座カテゴリの最新記事

%d人のブロガーが「いいね」をつけました。