29
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Android内にHttpサーバを立てて、ChromeCastでローカルファイルを再生する

Posted at

この記事では、Android端末内に保存されているファイルをChromeCastで再生する方法を説明します:grinning:

はじめに

ChromeCastでメディアを再生したい場合、CastするメディアのURLが必要です。
そのため、端末のローカルファイルをChromeCastで再生する場合には、ChromeCastから参照できる場所にファイルを公開しなければなりません。
この記事ではアプリ自身でHttpServerを立てることで、ChromeCastが端末のファイルにアクセスできるようにします!

処理の流れは下記のようになっています。
1. ローカルの音楽ファイルを取得する
2. サーバを起動し、ファイルを返却する
3. ファイルをChromeCastから再生する

1. ローカルの音楽ファイルを取得する

ContentResolverを使用して端末内(SDカード含む)の音楽ファイルを取得します。
アルバムアートの取得が少し複雑です。
参考:java - Most robust way to fetch album art in Android - Stack Overflow

MainActivity.java
    private List<MediaMetadataCompat> fetchMedia() {
        List<MediaMetadataCompat> mediaList = new ArrayList<>();
        Cursor mediaCursor = getContentResolver().query(
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                new String[]{
                        MediaStore.Audio.Media._ID,
                        MediaStore.Audio.Media.DATA,
                        MediaStore.Audio.Media.TITLE,
                        MediaStore.Audio.Media.ARTIST,
                        MediaStore.Audio.Media.ALBUM,
                        MediaStore.Audio.Media.ALBUM_ID},
                MediaStore.Audio.Media.IS_MUSIC + " != 0", null, null);
        if (mediaCursor != null && mediaCursor.moveToFirst()) {
            do {
                mediaList.add(buildMediaMetadataCompat(mediaCursor));
            } while (mediaCursor.moveToNext());
            mediaCursor.close();
        }
        return mediaList;
    }

    private MediaMetadataCompat buildMediaMetadataCompat(Cursor cursor) {
        return new MediaMetadataCompat.Builder()
                .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, cursor.getString(0))
                .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, cursor.getString(1))
                .putString(MediaMetadataCompat.METADATA_KEY_TITLE, cursor.getString(2))
                .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, cursor.getString(3))
                .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, cursor.getString(4))
                .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI,
                        getAlbumArtUri(cursor.getLong(5)).toString())
                .build();
    }

    private Uri getAlbumArtUri(long albumId) {
        Uri albumArtUri = Uri.parse("content://media/external/audio/albumart");
        return ContentUris.withAppendedId(albumArtUri, albumId);
    }

SDカードの中を探すのでManifest.permission.READ_EXTERNAL_STORAGEが必要です。

MainActivity.java
    private void checkPermissions() {
        if (Build.VERSION.SDK_INT >= 23) {
            ActivityCompat.requestPermissions(this, new String[]{
                    Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
        }
    }
AndroidManifest.xml
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

2. サーバを起動し、ファイルを返却する

HttpServerにNanoHttpdを使用します。

build.gradle
dependencies {
    compile 'com.nanohttpd:nanohttpd-webserver:2.1.1'
}

ポイントは3つです!
1. WifiManagerを使用して端末のIPアドレスを調べる
2. 空いているポートを探す
3. サーバを起動する

MainActivity.java
    private void startSever() {
        try {
            String ip = getWifiAddress();
            int port = findOpenPort(ip, 8080);
            mHttpServer = new HttpServer(port);
            mHttpServer.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String getWifiAddress() {
        WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE);
        WifiInfo wifiInfo = wifiManager.getConnectionInfo();
        int ipAddress = wifiInfo.getIpAddress();
        return ((ipAddress) & 0xFF) + "." +
                ((ipAddress >> 8) & 0xFF) + "." +
                ((ipAddress >> 16) & 0xFF) + "." +
                ((ipAddress >> 24) & 0xFF);
    }

    private int findOpenPort(String ip, int startPort) {
        final int timeout = 200;
        for (int port = startPort; port <= 65535; port++) {
            if (isPortAvailable(ip, port, timeout)) {
                return port;
            }
        }
        throw new RuntimeException("There is no open port.");
    }

    private boolean isPortAvailable(String ip, int port, int timeout) {
        try {
            Socket socket = new Socket();
            socket.connect(new InetSocketAddress(ip, port), timeout);
            socket.close();
            return false;
        } catch (Exception e) {
            return true;
        }
    }

wifiの情報を取得するので、Manifest.permission.ACCESS_WIFI_STATEが必要です

AndroidManifest.xml
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

NanoHTTPDは音楽とアルバムアートをそれぞれ返却する必要があります。
Uriに応じてレスポンスを分岐させます。

MainActivity.java
    private class HttpServer extends NanoHTTPD {
        final int mPort;
        MediaMetadataCompat mMedia;

        HttpServer(int port) throws IOException {
            super(port);
            mPort = port;
        }

        void setMedia(MediaMetadataCompat media) {
            mMedia = media;
        }

        @Override
        public Response serve(IHTTPSession session) {
            if (mMedia == null) {
                return new Response(NOT_FOUND, MIME_PLAINTEXT, "No music");
            }
            if (session.getUri().contains("image")) {
                return serveImage();
            } else {
                return serveMusic();
            }
        }

        private Response serveMusic() {
            InputStream stream = null;
            try {
                stream = new FileInputStream(
                        mMedia.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI));
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            return new Response(OK, "audio/mp3", stream);
        }

        private Response serveImage() {
            InputStream stream = null;
            try {
                stream = getContentResolver().openInputStream(Uri.parse(
                        mMedia.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI)));
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            return new Response(OK, "image/jpeg", stream);
        }
    }

3. ファイルをChromeCastから再生する

play-services-cast-frameworkを使用します

build.gradle
dependencies {
    compile 'com.google.android.gms:play-services-cast-framework:10.0.1'
}

ネットワーク上にChromeCastが存在する場合のみmenuにアイコンを表示します。

MainActivity.java
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.main, menu);
        try {
            CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                    menu, R.id.media_route_menu_item);
        } catch (RuntimeException e) {
            e.printStackTrace();
        }
        return true;
    }

再生ボタンが押されたら、ローカルファイルからランダムに1曲再生することにします。

MainActivity.java
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item != null && item.getItemId() == R.id.play) {
            List<MediaMetadataCompat> mediaList = fetchMedia();
            if (mediaList == null || mediaList.isEmpty()) {
                return true;
            }
            Collections.shuffle(mediaList);
            if (mHttpServer == null || !mHttpServer.isAlive()) {
                startSever();
            }
            String url = "http://" + getWifiAddress() + ":" + mHttpServer.mPort;
            mHttpServer.setMedia(mediaList.get(0));
            castMedia(url, mediaList.get(0));
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

ChromeCastに音楽を転送します。

MainActivity.java
    private void castMedia(String url, MediaMetadataCompat track) {
        CastSession castSession = CastContext.getSharedInstance(getApplicationContext()).getSessionManager()
                .getCurrentCastSession();
        if (castSession != null) {
            MediaInfo media = toCastMediaMetadata(url, track);
            RemoteMediaClient remoteMediaClient = castSession.getRemoteMediaClient();
            if (remoteMediaClient != null) {
                remoteMediaClient.load(media);
            }
        }
    }

    private MediaInfo toCastMediaMetadata(String url, MediaMetadataCompat track) {
        MediaMetadata mediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK);
        mediaMetadata.putString(MediaMetadata.KEY_TITLE,
                track.getDescription().getTitle() == null ? "" :
                        track.getDescription().getTitle().toString());
        mediaMetadata.putString(MediaMetadata.KEY_SUBTITLE,
                track.getDescription().getSubtitle() == null ? "" :
                        track.getDescription().getSubtitle().toString());
        mediaMetadata.putString(MediaMetadata.KEY_ALBUM_ARTIST,
                track.getString(MediaMetadataCompat.METADATA_KEY_ARTIST));
        mediaMetadata.putString(MediaMetadata.KEY_ALBUM_TITLE,
                track.getString(MediaMetadataCompat.METADATA_KEY_ALBUM));
        WebImage image = new WebImage(
                new Uri.Builder().encodedPath(url + "/image").build());
        // First image is used by the receiver for showing the audio album art.
        mediaMetadata.addImage(image);
        // Second image is used by Cast Companion Library on the full screen activity that is shown
        // when the cast dialog is clicked.
        mediaMetadata.addImage(image);

        return new MediaInfo.Builder(url)
                .setContentType("audio/mpeg")
                .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
                .setMetadata(mediaMetadata)
                .build();
    }

CastContextを用いてCastを実装しているのでOptionsProviderを定義する必要があります。

AndroidManifest.xml
        <meta-data
            android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
            android:value="com.sjn.demo.localfilecast.CastOptionsProvider"/>
CastOptionProvider.java
package com.sjn.demo.localfilecast;

import android.content.Context;

import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastOptions;
import com.google.android.gms.cast.framework.OptionsProvider;
import com.google.android.gms.cast.framework.SessionProvider;

import java.util.List;

public class CastOptionsProvider implements OptionsProvider {

    @Override
    public CastOptions getCastOptions(Context context) {
        return new CastOptions.Builder()
                .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
                .build();
    }

    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

最終的にMainActivity.javaはこのようになります:disappointed_relieved:

MainActivity.java
package com.sjn.demo.localfilecast;

import android.Manifest;
import android.content.ContentUris;
import android.database.Cursor;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.v4.app.ActivityCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;

import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.cast.framework.CastSession;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.common.images.WebImage;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import fi.iki.elonen.NanoHTTPD;

import static fi.iki.elonen.NanoHTTPD.Response.Status.NOT_FOUND;
import static fi.iki.elonen.NanoHTTPD.Response.Status.OK;

public class MainActivity extends AppCompatActivity {
    private HttpServer mHttpServer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        checkPermissions();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mHttpServer != null && mHttpServer.isAlive()) {
            mHttpServer.stop();
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.main, menu);
        try {
            CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                    menu, R.id.media_route_menu_item);
        } catch (RuntimeException e) {
            e.printStackTrace();
        }
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item != null && item.getItemId() == R.id.play) {
            List<MediaMetadataCompat> mediaList = fetchMedia();
            if (mediaList == null || mediaList.isEmpty()) {
                return true;
            }
            Collections.shuffle(mediaList);
            if (mHttpServer == null || !mHttpServer.isAlive()) {
                startSever();
            }
            String url = "http://" + getWifiAddress() + ":" + mHttpServer.mPort;
            mHttpServer.setMedia(mediaList.get(0));
            castMedia(url, mediaList.get(0));
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private class HttpServer extends NanoHTTPD {
        final int mPort;
        MediaMetadataCompat mMedia;

        HttpServer(int port) throws IOException {
            super(port);
            mPort = port;
        }

        void setMedia(MediaMetadataCompat media) {
            mMedia = media;
        }

        @Override
        public Response serve(IHTTPSession session) {
            if (mMedia == null) {
                return new Response(NOT_FOUND, MIME_PLAINTEXT, "No music");
            }
            if (session.getUri().contains("image")) {
                return serveImage();
            } else {
                return serveMusic();
            }
        }

        private Response serveMusic() {
            InputStream stream = null;
            try {
                stream = new FileInputStream(
                        mMedia.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI));
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            return new Response(OK, "audio/mp3", stream);
        }

        private Response serveImage() {
            InputStream stream = null;
            try {
                stream = getContentResolver().openInputStream(Uri.parse(
                        mMedia.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI)));
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            return new Response(OK, "image/jpeg", stream);
        }
    }

    private void startSever() {
        try {
            String ip = getWifiAddress();
            int port = findOpenPort(ip, 8080);
            mHttpServer = new HttpServer(port);
            mHttpServer.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String getWifiAddress() {
        WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE);
        WifiInfo wifiInfo = wifiManager.getConnectionInfo();
        int ipAddress = wifiInfo.getIpAddress();
        return ((ipAddress) & 0xFF) + "." +
                ((ipAddress >> 8) & 0xFF) + "." +
                ((ipAddress >> 16) & 0xFF) + "." +
                ((ipAddress >> 24) & 0xFF);
    }

    private int findOpenPort(String ip, int startPort) {
        final int timeout = 200;
        for (int port = startPort; port <= 65535; port++) {
            if (isPortAvailable(ip, port, timeout)) {
                return port;
            }
        }
        throw new RuntimeException("There is no open port.");
    }

    private boolean isPortAvailable(String ip, int port, int timeout) {
        try {
            Socket socket = new Socket();
            socket.connect(new InetSocketAddress(ip, port), timeout);
            socket.close();
            return false;
        } catch (Exception e) {
            return true;
        }
    }

    private void checkPermissions() {
        if (Build.VERSION.SDK_INT >= 23) {
            ActivityCompat.requestPermissions(this, new String[]{
                    Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
        }
    }

    private List<MediaMetadataCompat> fetchMedia() {
        List<MediaMetadataCompat> mediaList = new ArrayList<>();
        Cursor mediaCursor = getContentResolver().query(
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                new String[]{
                        MediaStore.Audio.Media._ID,
                        MediaStore.Audio.Media.DATA,
                        MediaStore.Audio.Media.TITLE,
                        MediaStore.Audio.Media.ARTIST,
                        MediaStore.Audio.Media.ALBUM,
                        MediaStore.Audio.Media.ALBUM_ID},
                MediaStore.Audio.Media.IS_MUSIC + " != 0", null, null);
        if (mediaCursor != null && mediaCursor.moveToFirst()) {
            do {
                mediaList.add(buildMediaMetadataCompat(mediaCursor));
            } while (mediaCursor.moveToNext());
            mediaCursor.close();
        }
        return mediaList;
    }

    private MediaMetadataCompat buildMediaMetadataCompat(Cursor cursor) {
        return new MediaMetadataCompat.Builder()
                .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, cursor.getString(0))
                .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, cursor.getString(1))
                .putString(MediaMetadataCompat.METADATA_KEY_TITLE, cursor.getString(2))
                .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, cursor.getString(3))
                .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, cursor.getString(4))
                .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI,
                        getAlbumArtUri(cursor.getLong(5)).toString())
                .build();
    }

    private Uri getAlbumArtUri(long albumId) {
        Uri albumArtUri = Uri.parse("content://media/external/audio/albumart");
        return ContentUris.withAppendedId(albumArtUri, albumId);
    }

    void castMedia(String url, MediaMetadataCompat track) {
        CastSession castSession = CastContext.getSharedInstance(getApplicationContext()).getSessionManager()
                .getCurrentCastSession();
        if (castSession != null) {
            MediaInfo media = toCastMediaMetadata(url, track);
            RemoteMediaClient remoteMediaClient = castSession.getRemoteMediaClient();
            if (remoteMediaClient != null) {
                remoteMediaClient.load(media);
            }
        }
    }

    private MediaInfo toCastMediaMetadata(String url, MediaMetadataCompat track) {
        MediaMetadata mediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK);
        mediaMetadata.putString(MediaMetadata.KEY_TITLE,
                track.getDescription().getTitle() == null ? "" :
                        track.getDescription().getTitle().toString());
        mediaMetadata.putString(MediaMetadata.KEY_SUBTITLE,
                track.getDescription().getSubtitle() == null ? "" :
                        track.getDescription().getSubtitle().toString());
        mediaMetadata.putString(MediaMetadata.KEY_ALBUM_ARTIST,
                track.getString(MediaMetadataCompat.METADATA_KEY_ARTIST));
        mediaMetadata.putString(MediaMetadata.KEY_ALBUM_TITLE,
                track.getString(MediaMetadataCompat.METADATA_KEY_ALBUM));
        WebImage image = new WebImage(
                new Uri.Builder().encodedPath(url + "/image").build());
        // First image is used by the receiver for showing the audio album art.
        mediaMetadata.addImage(image);
        // Second image is used by Cast Companion Library on the full screen activity that is shown
        // when the cast dialog is clicked.
        mediaMetadata.addImage(image);

        return new MediaInfo.Builder(url)
                .setContentType("audio/mpeg")
                .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
                .setMetadata(mediaMetadata)
                .build();
    }

}

おわりに

NanoHTTPDがとても使いやすかったです。アプリの機能としてだけでなく、開発中のデバッグ情報をプライベートネットワーク上に公開するような使い方もしてみたいと思いました。

今回のソースはsjnyag/localfilecast-demoで公開しています:smiley:

29
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?