モチベーション

YouTubeでプログラミングのライブ配信するときに、ニコニコ生放送ではお馴染みの「棒読みちゃん」が欲しかった。 けど、Mac環境だと使い勝手が悪いのでYouTubeのAPIとWebSpeechAPI(音声合成)でWebブラウザで動く棒読みちゃんを自分で作ってみることに。

趣味でDartを勉強しているのとYouTubeはgoogleだし相性もいいかなと思ってDart実装にチャレンジしてみた。

名前は、棒読みちゃんよりWebSpeechAPIの音声の人の方がしっかりした声なので、棒読みさんと名付ける。

regonn/bouyomi-san-youtube-live.dart

ローカルにDart環境が揃っていれば、Dart project scaffolding generatorのstagehandを使って web-simple モードで生成すればすぐに開発が始められる。

コード

main.dart

import 'dart:html';
import 'dart:async';
import 'package:googleapis_auth/auth_browser.dart' as auth;
import 'package:googleapis/youtube/v3.dart' as youtube;

DateTime lastMessagedAt = new DateTime.now();

void main() {
  InputElement apiKeyInput = querySelector('#api-key');
  InputElement channelIdInput = querySelector('#channel-id');
  ButtonElement setButton = querySelector('#set-button');

  displayOutput('APIキーとチャンネルを設定してください。');
  speak('起動しました。エーピーアイキーとチャンネルを設定してください。');

  setButton.onClick.listen((_) {
    var apiKey = apiKeyInput.value;
    var client = auth.clientViaApiKey(apiKey);
    var api = new youtube.YoutubeApi(client);
    api.search.list("id", channelId: channelIdInput.value, type: 'video', eventType: 'live')
      .then((youtube.SearchListResponse response) {
        var liveVideoId = null;
        if (response.items != null) {
          liveVideoId = response.items.first.id.videoId;
          api.videos.list("liveStreamingDetails,snippet", id: liveVideoId).then((youtube.VideoListResponse videoResponse) {
            var liveChatId = null;
            if (videoResponse.items != null) {
              var video = videoResponse.items.first;
              liveChatId = video.liveStreamingDetails.activeLiveChatId;
              var title = video.snippet.title;
              speak('$title のチャンネルに設定されました');
              displayOutput(title);
              lastMessagedAt = new DateTime.now();
              const duration = const Duration(seconds:5);
              new Timer.periodic(duration, (Timer t) => speakNewMessages(api, liveChatId));
            }
          });
        }
      })
      .catchError((e) {
        const errorText = 'ライブ情報の取得に失敗しました。入力情報が正しくない可能性または、ライブ放送が行われていない可能性があります。';
        speak(errorText);
        displayOutput(errorText);
      });
  });
}

void speakNewMessages(youtube.YoutubeApi api, String liveChatId) {
  api.liveChatMessages.list(liveChatId, 'snippet').then((youtube.LiveChatMessageListResponse messagesResponse) {
    if (messagesResponse.items != null) {
      var speakMessages = messagesResponse.items.where((item)=> item.snippet.publishedAt.isAfter(lastMessagedAt)).toList();
      for(youtube.LiveChatMessage message in speakMessages ){
        lastMessagedAt = message.snippet.publishedAt;
        speak(message.snippet.displayMessage);
      }
    }
  });
}

void speak(String text) {
  var u = new SpeechSynthesisUtterance();
  u.text = text;
  u.lang = 'ja-JP';
  u.rate = 1.4;
  window.speechSynthesis.speak(u);
}

void displayOutput(String text) {
  querySelector('#output').text = text;
}

LiveChatIdを取得するために、2回apiを叩いているためネストしてしまって読みにくいが、特に難しいことはしていない。

APIの仕様上、最低のコメント取得が200個なので、過去のものを読み込まないように lastMessagedAt を変数で持っておいて、ボタンを押して開始したタイミングだったり、読み上げたメッセージの時間で更新をかけて、同じものは読み上げないようにしている。

Dartを触ってみて

良いところ

一度はJavascriptの置き換えを狙った言語でもあって、とてもHTMLとの相性が良い。

WebSpeechAPIで読み上げる部分も

void speak(String text) {
  var u = new SpeechSynthesisUtterance();
  u.text = text;
  u.lang = 'ja-JP';
  u.rate = 1.4;
  window.speechSynthesis.speak(u);
}

のように簡単に記述できるし、クリックの管理も

ButtonElement setButton = querySelector('#set-button');
setButton.onClick.listen((_) {})

みたいに、jQueryを書かないで実装できてしまうので良かった。

外部のパッケージもAPI取得で使う二つしか使わなかった。

import 'package:googleapis_auth/auth_browser.dart' as auth;
import 'package:googleapis/youtube/v3.dart' as youtube;

悪いところ

ご存知の通り、Dart言語は一度は盛んになりかけて現在は下火なので、最新のコードサンプルが少なかったりライブラリが古いものが多い。

Google公式のものは比較的新しいが、googleapi のサンプルコードも三年前とかで、本当に動くのか不安になった。

デモサイト

コチラで実際に動きを確認できます。(実際に動かすにはYouTubeDataAPI v3が有効になっているAPIキーが必要です。)