2012年7月22日日曜日

[Android]Android向けDMRアプリに挑戦 その2

さて。その1でとりあえずひな形はできたので中身を実装していきます。

DMCから指定された音源を再生

まずは再生することを考えましょうってことで、音源の指定を受けるAPIの実装をば。
※ この実装ではMediaPlayerの状態管理が甘々なのでコピペして使っちゃいけません。サンプルにしたって酷いので、あぁとりあえずここで再生するためのコードを書けばいいんだな、だけを参考にしてください。
真似しちゃ駄目、ゼッタイ!

       @Override
       public void SetAVTransportURI(
                     long InstanceID,
                     String CurrentURI,
                     String CurrentURIMetaData)
       {
        Log.d(TAG, "SetAVTransportURI");
        if(mMp == null){
         mMp = new MediaPlayer();
         mMp.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
       
       @Override
       public void onCompletion(MediaPlayer mp) {
        // TODO Auto-generated method stub
        mp.reset();
        mCurrentPlayState = "STOPPED";
        StateChanged();
       }
      });
        }
        if(mMp.isPlaying()) {
         mMp.stop();
         mMp.reset();
        }
        try {
      mMp.setDataSource(CurrentURI);
     } catch (IllegalArgumentException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      Log.d(TAG, e.getMessage());
     } catch (IllegalStateException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      Log.d(TAG, e.getMessage());
     } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      Log.d(TAG, e.getMessage());
     }
        try {
      mMp.prepare();
     } catch (IllegalStateException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
     } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
     }
        mCurrentURI = CurrentURI;
        mCurrentURIMetaData = CurrentURIMetaData;
        String avt = "<Event xmlns = \"urn:schemas-upnp-org:metadata-1-0/AVT/\">\n";
           avt += "<InstanceID val=\"0\">\n";
           avt += "<AVTransportURI val=\"" + mCurrentURI + "\"/>\n";
           avt += "</InstanceID>\n";
           avt += "</Event>";
           root1.SetStateVariableValue_AVTransport_LastChange(avt);

           avt = "<Event xmlns = \"urn:schemas-upnp-org:metadata-1-0/AVT/\">\n";
           avt += "<InstanceID val=\"0\">\n";
           avt += "<NumberOfTracks val=\"" + "1" + "\"/>\n";
           avt += "<CurrentTrack val=\"" + "1" + "\"/>\n";
           avt += "<CurrentTrackMetaData val=\"" + StringEscapeUtils.escapeXml(mCurrentURIMetaData) + "\"/>\n";
           avt += "<CurrentTrackURI val=\"" + mCurrentURI + "\"/>\n";
           avt += "</InstanceID>\n";
           avt += "</Event>";
           root1.SetStateVariableValue_AVTransport_LastChange(avt);

           //mMp.start();

           //
           // You can return a UPnP Error code by throwing the following exception:
           //
           // throw (new UPnPInvokeException(statusCode, statusDescription));
           //
       }

ちなみに
        String avt = "<Event xmlns = \"urn:schemas-upnp-org:metadata-1-0/AVT/\">\n";
           avt += "<InstanceID val=\"0\">\n";
           avt += "<AVTransportURI val=\"" + mCurrentURI + "\"/>\n";
           avt += "</InstanceID>\n";
           avt += "</Event>";
           root1.SetStateVariableValue_AVTransport_LastChange(avt);

           avt = "<Event xmlns = \"urn:schemas-upnp-org:metadata-1-0/AVT/\">\n";
           avt += "<InstanceID val=\"0\">\n";
           avt += "<NumberOfTracks val=\"" + "1" + "\"/>\n";
           avt += "<CurrentTrack val=\"" + "1" + "\"/>\n";
           avt += "<CurrentTrackMetaData val=\"" + StringEscapeUtils.escapeXml(mCurrentURIMetaData) + "\"/>\n";
           avt += "<CurrentTrackURI val=\"" + mCurrentURI + "\"/>\n";
           avt += "</InstanceID>\n";
           avt += "</Event>";
           root1.SetStateVariableValue_AVTransport_LastChange(avt);
の部分は、DMCへ発行するイベントに関する処理です。ただ、コレに関してはひょっとするとうまく行ってないかもしれません・・・。その1の記事で、一部のアプリではうまく再生ができないといった原因がここにあると思っています。CurrentURIMetaDataはDIDL-LiteってなXSDで規定されるXML文字列なんですが、おそらくこいつもエスケープしなきゃならんのだろうってことでApache Common Languageライブラリですっけか、それのescapeXmlメソッドを使ってます。

ただ、<とかはエスケープしてる割に、「"」については"でエスケープしていないでLastChangeメソッドに渡してるので、そこがよく分かっていなかったり。"はしちゃいけないかもしれないのでそこがややこしい。。。

まぁ、とりあえずこれでDMCからの再生命令には答えられます。なんか再生したら、Androidが標準で対応している音源ならさくっと再生してくれます。たいていのアプリがhttp-getで飛ばしてくるのでそれ前提で作っちゃってます。MediaPlayerがURI対応してくれてるので楽でいいですね。とりあえずCurrentURI食わせればそのまま再生できて非常に楽。ほんとはエラー処理もっとちゃんとしないといけないんですけどね。

再生と一時停止はさくっとこんな感じでいいのかな? とりあえず動いてはいる。
       @Override
       public void Pause(
                     long InstanceID)
       {
        if(mMp != null) {
         if(mMp.isPlaying()) {
          mMp.pause();
         }
        }
           //
           // You can return a UPnP Error code by throwing the following exception:
           //
           // throw (new UPnPInvokeException(statusCode, statusDescription));
           //
       }
       @Override
       public void Play(
                     long InstanceID,
                     String Speed)
       {
        Log.d(TAG, "Play");
        if(mMp != null) {
         if(!mMp.isPlaying()) {
          mMp.start();
         }
        }
           //
           // You can return a UPnP Error code by throwing the following exception:
           //
           // throw (new UPnPInvokeException(statusCode, statusDescription));
           //
       }
あとはシーク位置を知らせるGetPositionInfoあたりを実装しておけばそれっぽくなる感じかな?
       @Override
       public void GetPositionInfo(
                     long InstanceID,
                     RefParameter<Long> Track,
                     RefParameter<String> TrackDuration,
                     RefParameter<String> TrackMetaData,
                     RefParameter<String> TrackURI,
                     RefParameter<String> RelTime,
                     RefParameter<String> AbsTime,
                     RefParameter<Integer> RelCount,
                     RefParameter<Integer> AbsCount)
       {
        Log.d(TAG, "GetPositionInfo");
        SimpleDateFormat sdt = new SimpleDateFormat("HH:mm:ss");
           // These are only sample values... 
           Track.value = (mMp == null) ? Long.valueOf((long)0) : Long.valueOf((long)1);
           Date TD = (mMp == null) ? new Date(- (32400 * 1000)) : new Date(mMp.getDuration() - (32400 * 1000));
           TrackDuration.value = sdt.format(TD);
           TrackMetaData.value = mCurrentURIMetaData;
           TrackURI.value = mCurrentURI;
           TD = (mMp == null) ? new Date(- (32400 * 1000)) : new Date(mMp.getCurrentPosition() - (32400 * 1000));
           RelTime.value = sdt.format(TD);
           AbsTime.value = RelTime.value;
           RelCount.value = (mMp == null) ? 0 : mMp.getCurrentPosition() / 1000;
           AbsCount.value = RelCount.value;
           // You can return a UPnP Error code by throwing the following exception:
           //
           // throw (new UPnPInvokeException(statusCode, statusDescription));
           //
       }
ただ、ここの処理も結構甘いです。本来ならMediaPlayerが非nullであっても、再生停止状態であれば(&neq;一時停止)getDurationがエラー吐いてるので処理としてはよろしくないです。

とりあえず、これくらい実装しておけばいくつかのDMCからは一応使えます。

あとはRenderingControlのところでボリューム関連を実装したらそれっぽくなるんじゃないかな?
まぁボリュームはあんまりいじることないだろうから実装してなくても大して困らないかも

さて、これでひと通り実装できたわけですが、残念ながらイベント処理でまだまだ不足があるようです。
ONKYOが自身のアンプ向けにOnkyo Remoteってアプリを提供してるんですが、こいつで再生しようとしてもなぜか接続状態を維持しきれません。
SetAVTransportってやったあと、GetPositionInfoを怒涛の勢いで呼び出してるんですが、なぜかシーク位置とかを認識してくれない。。。

もちろん、Onkyo RemoteはONKYOのアンプ以外を考えて作られているわけじゃないので、うまく動かないところに文句を垂れるべきではないのでしょうが、他のDMR(PioneerのN-50とか)には問題なく再生できるんですよねぇ。

ということは、私のDMRの実装がまずいという話に。APIは叩かれたらすぐ分かるんですがイベントはよー分からんのですよねぇ・・・。
かくなる上はDMCも自分で作って、自作DMRとやりとりをするような形でデバッグを進めるか・・・。

まぁ、正直そこはしんどいのであんまり気は進まないんですがねー。 今の状態でもある程度動くには動くので、MediaPlayerの状態遷移をもうちょっとしっかり管理したら次は処理全体のサービス化でも考えましょうかね。

うーん、にしてもなんでOnkyoRemoteにはうまくGetPositionInfoの情報が返されてないのかなぁ。。。