google スマートホーム

[Windows10]Google Homeに音楽(mp3)を連続再生させる

更新日:

スポンサードリンク

はじめに

前回「[Windows10]Google Homeに指定の言葉をしゃべらせる+音楽(mp3)を再生させる」で音楽を単発では再生できるけど連続再生できないということに気が付きました。

今回は試行錯誤して何とか連続再生できたのでそこまでのやり方を書いておきます。

やりたいこと

ローカルサーバーのMP3ファイルを連続+ランダム再生をしようと思います。

前提として前回同様にWindows10のローカルパソコン(192.168.1.10)のbgmフォルダに再生したいMP3ファイル(複数)を保存してそれらのファイルを連続再生し続けます。ついでに飽きが来ないようにランダム再生もしたいです。

そして、もし止めたくなったらGoogle Homeに「止めて」などと言えば停止するようにします。

やり方はgoogle-home-notifier.jsをベースにしつつNode.jsのスクリプトを作成して何とか実現させたいと思います。
以下のサイト様の記事も参考にさせていただきました。

ソースコード確認

付属のgoogle-home-notifier.jsのソースコードでは音楽を連続再生することができません。ソースコードを見ていくと次の2つがその理由だと思われます。

  • Google Homeとの接続をすぐに切断しているため次の曲を流す前に処理が終了してしまう
  • 曲の再生が終わってもそれを検知していない

1つ目に関しては、Google Homeとの接続を終了する処理(client.close())をコメントアウトして再生終了後の位置に移動すれば一応クリアできます。

2つ目はもともとのソースでは使われていないのですが内部の状態遷移をトリガとするコールバック(イベント)が登録できるのでそれを使います。

サンプルソース

付属のgoogle-home-notifier.jsに手を加えたmy-google-home-notifier.jsを作成して連続再生に対応できるようにしました。また、それを呼び出すmusic.jsというスクリプトも作成しました。

google-home-notifier.jsの変更した個所はonDeviceUpメソッドの中です。
曲が終了するときに呼ばれるコールバック関数にGoogle Homeとの接続を終了する処理(client.close())を呼ぶように移動させました。

そしてさらに再生終了のコールバックが呼ばれたら次の曲を再生しました。そのためにonDeviecUpに登録されているコールバックには次の曲を再生する処理を登録して、それを呼び出すように変更しました。
onDeviceUpに登録するコールバックはmusic.js側で用意しています。

my-google-home-notifier.js(変更箇所)

var onDeviceUp = function(host, url, callback) {
  var client = new Client();

  client.connect(host, function() {
    client.launch(DefaultMediaReceiver, function(err, player) {

      var media = {
        contentId: url,
        contentType: 'audio/mp3',
        streamType: 'BUFFERED' // or LIVE
      };
      player.load(media, { autoplay: true }, function(err, status) {
        //client.close();
        //callback('Device notified');
      });
      player.on('status', function (status) {
          //console.log(status);
          if(status.playerState=='IDLE') {
              if (status.extendedStatus != undefined && status.extendedStatus.playerState == 'LOADING' ) {
              
              // 局再生が終了したら切断して次の曲を呼ぶ
              } else if (status.idleReason=='FINISHED') {
              	client.close();
              	callback();
              }
          }
          // 停止命令が来たら切断する
          if(status.playerState=='PAUSED') {
              client.close();
          }
      });    
    });
  });

  client.on('error', function(err) {
    console.log('Error: %s', err.message);
    client.close();
    callback('error');
  });
};

my-google-home-notifier.js(全文)

var Client = require('castv2-client').Client;
var DefaultMediaReceiver = require('castv2-client').DefaultMediaReceiver;
var mdns = require('mdns');
var browser = mdns.createBrowser(mdns.tcp('googlecast'));
var deviceAddress;
var language;

var device = function(name, lang = 'en') {
    device = name;
    language = lang;
    return this;
};

var ip = function(ip, lang = 'en') {
  deviceAddress = ip;
  language = lang;
  return this;
}

var googletts = require('google-tts-api');
var googlettsaccent = 'us';
var accent = function(accent) {
  googlettsaccent = accent;
  return this;
}

var notify = function(message, callback) {
  if (!deviceAddress){
    browser.start();
    browser.on('serviceUp', function(service) {
      console.log('Device "%s" at %s:%d', service.name, service.addresses[0], service.port);
      if (service.name.includes(device.replace(' ', '-'))){
        deviceAddress = service.addresses[0];
        getSpeechUrl(message, deviceAddress, function(res) {
          callback(res);
        });
      }
      browser.stop();
    });
  }else {
    getSpeechUrl(message, deviceAddress, function(res) {
      callback(res);
    });
  }
};

var play = function(mp3_url, callback) {
  if (!deviceAddress){
    browser.start();
    browser.on('serviceUp', function(service) {
      console.log('Device "%s" at %s:%d', service.name, service.addresses[0], service.port);
      if (service.name.includes(device.replace(' ', '-'))){
        deviceAddress = service.addresses[0];
        getPlayUrl(mp3_url, deviceAddress, function(res) {
          callback(res);
        });
      }
      browser.stop();
    });
  }else {
    getPlayUrl(mp3_url, deviceAddress, function(res) {
      callback(res);
    });
  }
};

var getSpeechUrl = function(text, host, callback) {
  googletts(text, language, 1, 1000, googlettsaccent).then(function (url) {
    onDeviceUp(host, url, function(res){
      callback(res)
    });
  }).catch(function (err) {
    console.error(err.stack);
  });
};

var getPlayUrl = function(url, host, callback) {
    onDeviceUp(host, url, function(res){
      callback(res)
    });
};

var onDeviceUp = function(host, url, callback) {
  var client = new Client();

  client.connect(host, function() {
    client.launch(DefaultMediaReceiver, function(err, player) {

      var media = {
        contentId: url,
        contentType: 'audio/mp3',
        streamType: 'BUFFERED' // or LIVE
      };
      player.load(media, { autoplay: true }, function(err, status) {
        //client.close();
        //callback('Device notified');
      });
      player.on('status', function (status) {
          //console.log(status);
          if(status.playerState=='IDLE') {
              if (status.extendedStatus != undefined && status.extendedStatus.playerState == 'LOADING' ) {
              
              // 局再生が終了したら切断して次の曲を呼ぶ
              } else if (status.idleReason=='FINISHED') {
              	client.close();
              	callback();
              }
          }
          // 停止命令が来たら切断する
          if(status.playerState=='PAUSED') {
              client.close();
          }
      });    
    });
  });

  client.on('error', function(err) {
    console.log('Error: %s', err.message);
    client.close();
    callback('error');
  });
};

exports.ip = ip;
exports.device = device;
exports.accent = accent;
exports.notify = notify;
exports.play = play;

ローカルPCの音楽ファイルを取得して連続再生させるスクリプトmusic.jsについてです。

処理内容は音楽ファイルのURL配列を作ってそれを順々に再生するようにしているだけです。
冒頭のurlには音楽ファイルのあるURLを、pathには音楽ファイルのあるローカルアドレスのパスを指定します。これらは表現は異なるものの同じディレクトリを指し示しています。

このソースコード中のURLやパスは自分のものに書き換えてください。

music.js

function Music() {
	var googlehome = require('./my-google-home-notifier');
	// ローカルサーバーのURL
	var url = "http://192.168.1.10/bgm/"; 
	// ローカルサーバーのパス(音楽ファイル名取得のため)
	var path = "C:\\Bitnami\\redmine-xxxxx\\apache2\\htdocs\\bgm";
	var musicFiles = []; // 音楽ファイル名配列
	var musicIndex = 0; // 再生箇所を指すインデックス
	
	// 音楽ファイルパスのシャッフル
	var shuffleMusicFiles = function() {
	    // シャッフル
		for(var i = musicFiles.length - 1; i > 0; i--){
		    var r = Math.floor(Math.random() * (i + 1));
		    var tmp = musicFiles[i];
		    musicFiles[i] = musicFiles[r];
		    musicFiles[r] = tmp;
		}
	}
		
	// mp3ファイル取得
	var getMusicFiles = function(callback) {
		var fs = require('fs');
		fs.readdir(path, function(err, files){
		    if (err) throw err;
		    var idx = 0;
		    for (var i = 0 ; i < files.length ; i++) {
				var type = files[i].split('.');
				if (type[type.length - 1].toLowerCase() == 'mp3') {
		    		musicFiles[idx++] = files[i];
		    	}
		    }
		    // シャッフル
		    shuffleMusicFiles();
		    callback();
		});
	}
		
	// 初期化
	var init = function(callback) {		
		var language = 'ja'; 
		googlehome.device('Google Home', language);
		getMusicFiles(callback);
	}
	
	// 再生
	var play = function(url,callback) {
		googlehome.play(url, callback);
	}
	
	// 次の音楽を再生
	var next = function() {
		// 1周したら終了する
		if(musicIndex>=musicFiles.length) {
			return;
		}
		
		// 再帰的に再生する
		play(url + musicFiles[musicIndex++],function(){ next(); });
	}
	
	// メイン処理
	this.main = function() {
		init(function(){
			next();
		});
	}
}

var m = new Music()
m.main();

正直、Node.jsのお作法がよくわかっていないので結構適当に作成してしまいました。一応動くものができたので良しとしています。

実行

コマンドプロンプトを立ち上げてmy-google-home-notifier.jsとmusic.jsのあるフォルダまで移動してmusic.jsを動かすだけです。

node music.js

この辺は前回と同じなのでわからなくなったらそちらを参考にしてください。

(参考)状態遷移

状態遷移は大まかにIDLEとPLAYING、PAUSEの3つ確認しています。状態が遷移したときにplayerのstatusイベントが呼ばれるようです。

実際に状態遷移時にそのステータス変数をログ出力して確認したところおおむね以下のようなときに遷移するようでした。

IDLE:読み込み時や再生終了時
PLAYING:再生時
PAUSED:停止時(Google Homeに「止めて」などと呼びかけたとき遷移)

IDLEの中にも複数の状態があるようですが、終了時はプロパティのidleReasonがFINISHEDになることで判別ができます。
先ほどのソース本文ではコメントアウトしていますが、ステータスをログ出力して確認しました。具体的には以下のような値が出てきます。

・曲の読み込みを開始したとき(IDLE)


{ mediaSessionId: 1,
playbackRate: 1,
playerState: 'IDLE',
currentTime: 0,
supportedMediaCommands: 274447,
volume: { level: 1, muted: false },
activeTrackIds: [],
media:
{ contentId: 'http://192.168.1.10/bgm/sample1.mp3',
contentType: 'audio/mp3',
streamType: 'BUFFERED' },
currentItemId: 1,
extendedStatus:
{ playerState: 'LOADING',
media:
{ contentId: 'http://192.168.1.10/bgm/sample1.mp3',
contentType: 'audio/mp3',
streamType: 'BUFFERED' } },
repeatMode: 'REPEAT_OFF' }

・曲が再生されたとき(PLAYING)

{ mediaSessionId: 1,
playbackRate: 1,
playerState: 'PLAYING',
currentTime: 0.777506,
supportedMediaCommands: 274447,
volume: { level: 1, muted: false },
activeTrackIds: [],
currentItemId: 1,
repeatMode: 'REPEAT_OFF' }

・曲の再生が終わったとき(IDLEだがidleReasonがFINISHEDになる)


{ mediaSessionId: 1,
playbackRate: 1,
playerState: 'IDLE',
currentTime: 0,
supportedMediaCommands: 274447,
volume: { level: 1, muted: false },
currentItemId: 1,
idleReason: 'FINISHED' }

・止めたとき(PAUSED)

{ mediaSessionId: 1,
playbackRate: 1,
playerState: 'PAUSED',
currentTime: 3.720974,
supportedMediaCommands: 274447,
volume: { level: 1, muted: false },
activeTrackIds: [],
currentItemId: 1,
repeatMode: 'REPEAT_OFF' }

参考までに紹介しました。

Google Home経由で再生する

IFTTTと連携することで例えば「BGMを再生して」などのキーワードで音楽を再生できるようになります。
ただし、音楽ファイルはローカルPCの中に入っているため何かしらの工夫が必要です。

前回「IFTTT+Slack+HubotでGoogle Homeとおしゃべりができるようにする[Windows10]」でSlackとHubotとIFTTTを連携してそれをできるようにしたのでそちらを参考にしてください。

まとめ

Google Homeで音楽ファイルを連続再生することができました。

※一応私のもとでは動いていますが、もし使用する際は自己責任でお願いします。ソースの内容を確認の上使ってください。

(追記)
サムネ画像は3DCGで作成してみました

スポンサードリンク

-google, スマートホーム

Copyright© めめんと , 2019 All Rights Reserved Powered by AFFINGER5.