HTTP Live Streaming on Android from a Dynamically Generated Playlist

2.4k Views Asked by At

I am creating an Android app that streams videos using the Http Live Streaming protocol from a web application. Currently I am streaming the video playlist and segments, which were created by Amazon Elastic Transcoder, from an Amazon S3 bucket. This works perfectly using a simple VideoView and setting the video path to the URL of the .m3u8 playlist on S3.

I have requirements to use Amazon CloudFront for delivery and restrict all public access to the S3 buckets where the playlist and segments are stored. Based on my research, the only way to do this is to dynamically generate the HLS playlist to contain properly signed URLs for the segments.

Rather than generate the playlist and store it serverside, which would require periodic clean up of resources, my current attempt at a solution is the following. The Android app will retrieve the stream information and the media playlists so they can be recreated locally.

For example, we're streaming a video that has two streams: low quality and high quality. The information about the stream (bandwidth, codecs, etc) will be returned with the complete HLS playlist for each of these streams. The key thing here is the media playlists will have properly signed URLs for the video segments. The Android app will write these media playlists out to local temp files and generate the variant playlist. So the local filesystem would have the following files:

Variant playlist:

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1280000
low.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=7680000
hi.m3u8

low.m3u8 file:

#EXTM3U
#EXT-X-TARGETDURATION:8
#EXT-X-MEDIA-SEQUENCE:2680
#EXTINF:8,
https://priv.example.com/fileSequence2680-low.ts
#EXTINF:8,
https://priv.example.com/fileSequence2681-low.ts
#EXTINF:8,
https://priv.example.com/fileSequence2682-low.ts

hi.m3u8 file:

#EXTM3U
#EXT-X-TARGETDURATION:8
#EXT-X-MEDIA-SEQUENCE:2680
#EXTINF:8,
https://priv.example.com/fileSequence2680-hi.ts
#EXTINF:8,
https://priv.example.com/fileSequence2681-hi.ts
#EXTINF:8,
https://priv.example.com/fileSequence2682-hi.ts

I have proven this will work with a video player (VLC) that supports HLS.

I'm generating a proper variant playlist and attempting to set that as the path for the VideoView. I have tried using both setVideoPath and setVideoURI but to no avail. The only interesting difference in logcat between using the dynamically generated playlist and streaming directly from S3 is the MediaPlayer instance that is created. When streaming from a local file (file://), AwesomePlayer is instantiated. When streaming from the internet over HTTPS (https://), NuPlayer is instantiated. The error I get with the dynamically generated playlist is:

ERROR/AwesomePlayer(1837): setDataSource_l() extractor is NULL, return UNKNOWN_ERROR

After spending a few hours digging through the Android source code, I found a property named media.stagefright.use-nuplayer that will force NuPlayer to always be used if set. However, there doesn't seem to be a clean way to set this native system property from Java.

Is there some way at the application layer I can force NuPlayer to be used or another way to achieve what I want?

I am testing this on a Samsung Galaxy S2 running Android 4.1.2.

Code:

public class VideoPlayerActivity extends Activity
{
    private static final String TAG = VideoPlayerActivity.class.getCanonicalName();

    // URL intentionally changed to something generic to hide internal resources
    private static final String BASE_URL = "https://s3.amazonaws.com/bucket/";

    private static final String VARIANT_PLAYLIST_PATH = BASE_URL + "variant.m3u8";
    private static final String MEDIA_PLAYLIST_PATH = BASE_URL + "media1.m3u8";

    private static final String LINE_SEPARATOR = System.getProperty("line.separator");

    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_video_player);

        File variantPlaylist = createVariantPlaylist();

        final VideoView videoView = (VideoView) findViewById(R.id.videoView);
        videoView.setVideoPath(variantPlaylist.getAbsolutePath());

        MediaController mediaController = new MediaController(this);
        mediaController.setAnchorView(videoView);

        videoView.setMediaController(mediaController);

        videoView.setOnPreparedListener( new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                Log.i(TAG, "Duration = " + videoView.getDuration());
            }
        });

        videoView.start();
    }

    private File createVariantPlaylist() {
        File variantPlaylist = null;

        try {
            File directory = this.getFilesDir();
            variantPlaylist = File.createTempFile("variant", ".m3u8", directory);

            FileWriter fileWriter = new FileWriter(variantPlaylist.getAbsoluteFile());
            BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);

            bufferedWriter.write("#EXTM3U");
            bufferedWriter.write(LINE_SEPARATOR);
            bufferedWriter.write("#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=400x170,CODECS=\"avc1.42001e,mp4a.40.2\",BANDWIDTH=474000");
            bufferedWriter.write(LINE_SEPARATOR);
            bufferedWriter.write(MEDIA_PLAYLIST_PATH);

            bufferedWriter.close();
        }
        catch (IOException ioe) {
            Log.e(TAG, "Error occurred creating variant playlist");
            Log.e(TAG, ioe.getMessage());
        }

        return variantPlaylist;
    }
}
0

There are 0 best solutions below