brianwernick / ExoMedia

An Android ExoPlayer wrapper to simplify Audio and Video implementations
Apache License 2.0
2.14k stars 378 forks source link

HLS AES Key inside m3u8 playlists #433

Closed jboisjo closed 7 years ago

jboisjo commented 7 years ago

Hi Brian,

Do you have any idea how i can tell to the player that there's a AES Key inside the playlist, so he can see it and play it if my user is auth?

EXT-X-KEY:METHOD=AES-128 -> (the key inside a m3u8 file)

Thank you!!

brianwernick commented 7 years ago

There is the metadata listener (setMetadataListener()) that may include that information

jboisjo commented 7 years ago

How do you suggest that i approach it with the player? I did this, but nothing seems to be printing on the listener or starting my m3u8 with the key inside...

emVideoView.setVideoURI(Uri.parse(streamURL), getRendererBuilder(MediaSourceType.HLS, Uri.parse(streamURL), "UserAgent"));

        emVideoView.setId3MetadataListener(new Id3MetadataListener() {
            @Override
            public void onId3Metadata(List<Id3Frame> metadata) {
                emVideoView.start();
                Log.d("metadata", "" + metadata);

            }
        });
jboisjo commented 7 years ago

im having this error when i'm playing my playlist....

04-21 15:01:26.573 950-1116/ E/ExoPlayerImplInternal: Internal track renderer error.
                                                                                   com.google.android.exoplayer.ExoPlaybackException: com.google.android.exoplayer.upstream.HttpDataSource$InvalidResponseCodeException: Response code: 403
                                                                                       at com.google.android.exoplayer.SampleSourceTrackRenderer.maybeThrowError(SampleSourceTrackRenderer.java:262)
                                                                                       at com.google.android.exoplayer.SampleSourceTrackRenderer.maybeThrowError(SampleSourceTrackRenderer.java:148)
                                                                                       at com.google.android.exoplayer.ExoPlayerImplInternal.incrementalPrepareInternal(ExoPlayerImplInternal.java:273)
                                                                                       at com.google.android.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:203)
                                                                                       at android.os.Handler.dispatchMessage(Handler.java:98)
                                                                                       at android.os.Looper.loop(Looper.java:148)
                                                                                       at android.os.HandlerThread.run(HandlerThread.java:61)
                                                                                       at com.google.android.exoplayer.util.PriorityHandlerThread.run(PriorityHandlerThread.java:40)
                                                                                    Caused by: com.google.android.exoplayer.upstream.HttpDataSource$InvalidResponseCodeException: Response code: 403
                                                                                       at com.google.android.exoplayer.upstream.DefaultHttpDataSource.open(DefaultHttpDataSource.java:208)
                                                                                       at com.google.android.exoplayer.upstream.DefaultUriDataSource.open(DefaultUriDataSource.java:133)
                                                                                       at com.google.android.exoplayer.chunk.DataChunk.load(DataChunk.java:85)
                                                                                       at com.google.android.exoplayer.upstream.Loader$LoadTask.run(Loader.java:222)
                                                                                       at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:423)
                                                                                       at java.util.concurrent.FutureTask.run(FutureTask.java:237)
                                                                                       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
                                                                                       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
                                                                                       at java.lang.Thread.run(Thread.java:818)
04-21 15:01:26.577 950-950/ E/VC5: NATIVE_WINDOW_MIN_UNDEQUEUED_BUFFERS = 1
brianwernick commented 7 years ago

What is your MediaDrmCallback implementation? (what you are passing to setDrmCallback())

jboisjo commented 7 years ago

nothing for now, haven't setup the DRM yet...:/ i just need the player to read my Encrypted HLS.

brianwernick commented 7 years ago

Oh, my mistake; for some reason I was thinking you were trying to get DRM setup.

To make sure we are on the same page: You have a manifest url (*.m3u8) that is not encrypted but contains the key for the encrypted *.ts segments it references? If that's the case you will need to provide a DataSource that handles the AES 128 decryption. In ExoMedia 3.x you will need to create a custom HlsRenderBuilder and in 4.x you can call ExoMedia.setHttpDataSourceFactoryProvider((userAgent, listener) -> {/*todo*/}));

jboisjo commented 7 years ago

Yes exactly!

The only way i can provide a DataSource in 3.x is with a HlsRenderBuilder, but the only function that is buildRenderers().

I tried to create an instance of RenderBuilder like this...

protected RenderBuilder getRendererBuilder(MediaSourceType renderType, Uri uri, String userAgent) {
        switch (renderType) {
            case HLS:  //NOTE: this is your modified HlsRenderBuilder that customizes the HlsTrackSelector
                return new HlsRenderBuilder(getContext().getApplicationContext(), userAgent, uri.toString());
            case DASH:
                return new DashRenderBuilder(getContext().getApplicationContext(), userAgent, uri.toString());
            case SMOOTH_STREAM:
                return new SmoothStreamRenderBuilder(getContext().getApplicationContext(), userAgent, uri.toString());
            default:
                return new RenderBuilder(getContext().getApplicationContext(), userAgent, uri.toString());
        }
    }

But i don't know where i can provide a DataSource in the HLS case... And this is where i get my m3u8 playlist

 public void playVideo(final int livetrack) {
emVideoView.setVideoURI(Uri.parse(streamURL), getRendererBuilder(MediaSourceType.HLS, Uri.parse(streamURL), "UserAgent"));
}

Thank you so much for helping me btw!! :)

brianwernick commented 7 years ago

You will need to extend the HlsRenderBuilder and override the createManifestDataSource method.

Also, if you pass in a RenderBuilder you can pass in null for the URI to setVideoUri

brianwernick commented 7 years ago
public class AesHlsRenderBuilder extends HlsRenderBuilder {
    @Override
    protected UriDataSource createManifestDataSource(Context context, String userAgent) {
        // TODO: Provide your authenticated data source
        return new DefaultUriDataSource(context, userAgent);
    }

    @Override
    protected DataSource createDataSource(Context context, TransferListener transferListener, String userAgent) {
        // TODO: Provide your AES 128 decrypting data source
        return new DefaultUriDataSource(context, transferListener, userAgent, true);
    }
}

then emVideoView.setVideoURI(null, new AesHlsRenderBuilder(getContext().getApplicationContext(), "UserAgent", Uri.parse(streamURL)));

jboisjo commented 7 years ago

still nothing when i put null for the URI to setVideoUri is it normal?

Also, none of my streams is working when i extend the HlsRenderBuilder even the one without a key in the playlist.

:(

jboisjo commented 7 years ago

Ok, my playlists with no keys are working if i put the setURI to my streams otherwise its not working...but the one with the key inside is not working..

04-24 13:44:25.603 3345-7721/ E/ExoPlayerImplInternal: Internal track renderer error.
                                                                                    com.google.android.exoplayer.ExoPlaybackException: com.google.android.exoplayer.upstream.HttpDataSource$InvalidResponseCodeException: Response code: 403
                                                                                        at com.google.android.exoplayer.SampleSourceTrackRenderer.maybeThrowError(SampleSourceTrackRenderer.java:262)
                                                                                        at com.google.android.exoplayer.SampleSourceTrackRenderer.maybeThrowError(SampleSourceTrackRenderer.java:148)
                                                                                        at com.google.android.exoplayer.ExoPlayerImplInternal.incrementalPrepareInternal(ExoPlayerImplInternal.java:273)
                                                                                        at com.google.android.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:203)
                                                                                        at android.os.Handler.dispatchMessage(Handler.java:98)
                                                                                        at android.os.Looper.loop(Looper.java:148)
                                                                                        at android.os.HandlerThread.run(HandlerThread.java:61)
                                                                                        at com.google.android.exoplayer.util.PriorityHandlerThread.run(PriorityHandlerThread.java:40)
                                                                                     Caused by: com.google.android.exoplayer.upstream.HttpDataSource$InvalidResponseCodeException: Response code: 403
                                                                                        at com.google.android.exoplayer.upstream.DefaultHttpDataSource.open(DefaultHttpDataSource.java:208)
                                                                                        at com.google.android.exoplayer.upstream.DefaultUriDataSource.open(DefaultUriDataSource.java:133)
                                                                                        at com.google.android.exoplayer.chunk.DataChunk.load(DataChunk.java:85)
                                                                                        at com.google.android.exoplayer.upstream.Loader$LoadTask.run(Loader.java:222)
                                                                                        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:423)
                                                                                        at java.util.concurrent.FutureTask.run(FutureTask.java:237)
                                                                                        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
                                                                                        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
                                                                                        at java.lang.Thread.run(Thread.java:818)
04-24 13:44:25.617 3345-3345/ E/VC5: NATIVE_WINDOW_MIN_UNDEQUEUED_BUFFERS = 1
brianwernick commented 7 years ago

Are you sure the manifest file doesn't require a session cookie (login)?

jboisjo commented 7 years ago

yes it needs but i can't reproduce it the connection msg...now i'm always getting this...

04-24 13:58:58.256 18085-18368/com.ebox.jaytv.androidtv_ebox W/AudioCapabilities: Unsupported mime audio/ac3
04-24 13:58:58.280 18085-18368/com.ebox.jaytv.androidtv_ebox I/VideoCapabilities: Unsupported profile 4 for video/mp4v-es
04-24 13:58:58.284 18085-18368/com.ebox.jaytv.androidtv_ebox W/VideoCapabilities: Unrecognized profile/level 0/0 for video/mpeg2
04-24 13:58:58.284 18085-18368/com.ebox.jaytv.androidtv_ebox W/VideoCapabilities: Unrecognized profile/level 0/2 for video/mpeg2
04-24 13:58:58.284 18085-18368/com.ebox.jaytv.androidtv_ebox W/VideoCapabilities: Unrecognized profile/level 0/3 for video/mpeg2
04-24 13:59:01.754 18085-18368/com.ebox.jaytv.androidtv_ebox E/ExoPlayerImplInternal: Internal track renderer error.
                                                                                      com.google.android.exoplayer.ExoPlaybackException: com.google.android.exoplayer.upstream.HttpDataSource$InvalidResponseCodeException: Response code: 403
                                                                                          at com.google.android.exoplayer.SampleSourceTrackRenderer.maybeThrowError(SampleSourceTrackRenderer.java:262)
                                                                                          at com.google.android.exoplayer.SampleSourceTrackRenderer.maybeThrowError(SampleSourceTrackRenderer.java:148)
                                                                                          at com.google.android.exoplayer.ExoPlayerImplInternal.incrementalPrepareInternal(ExoPlayerImplInternal.java:273)
                                                                                          at com.google.android.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:203)
                                                                                          at android.os.Handler.dispatchMessage(Handler.java:98)
                                                                                          at android.os.Looper.loop(Looper.java:148)
                                                                                          at android.os.HandlerThread.run(HandlerThread.java:61)
                                                                                          at com.google.android.exoplayer.util.PriorityHandlerThread.run(PriorityHandlerThread.java:40)
                                                                                       Caused by: com.google.android.exoplayer.upstream.HttpDataSource$InvalidResponseCodeException: Response code: 403
                                                                                          at com.google.android.exoplayer.upstream.DefaultHttpDataSource.open(DefaultHttpDataSource.java:208)
                                                                                          at com.google.android.exoplayer.upstream.DefaultUriDataSource.open(DefaultUriDataSource.java:133)
                                                                                          at com.google.android.exoplayer.chunk.DataChunk.load(DataChunk.java:85)
                                                                                          at com.google.android.exoplayer.upstream.Loader$LoadTask.run(Loader.java:222)
                                                                                          at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:423)
                                                                                          at java.util.concurrent.FutureTask.run(FutureTask.java:237)
                                                                                          at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
                                                                                          at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
                                                                                          at java.lang.Thread.run(Thread.java:818)
brianwernick commented 7 years ago

That's the same message Response code: 403. You will need to use an authenticated DataSource as well

jboisjo commented 7 years ago

yes ur right, but i can't even reproduce the unable to login to mykey :(. It's always code: 403 now

jboisjo commented 7 years ago

one thing i don't understand with all this is why do i have to @Override createManifestDataSource when it looks like its only using for audio...

//Create the Sample Source to be used by the Audio Renderer
            DataSource dataSourceAudio = new DefaultUriDataSource(context, bandwidthMeter, userAgent);

Sorry to bug you like this with all my questions lol and thanks for your help again.

brianwernick commented 7 years ago

I've updated my example above.

There are 2 methods you will need to override; createManifestDataSource which will need to be authenticated to access your *.m3u8 manifest file and createDataSource which is what will be used for the .ts segments. The createDataSource is used for more than just audio though the line you posted looks like a bug in the 3.x releases of ExoMedia as it should be DataSource dataSourceAudio = createDataSource(context, bandwidthMeter, userAgent);

It's probably worth mentioning that the official 4.0.0 release will most likely happen this week so it'll probably be more beneficial to just use the preview for now

jboisjo commented 7 years ago

Ok Perfect! i'll try and let you know if there's something! Thank you very very much!!

jboisjo commented 7 years ago

Just to let you know the difference inside my m3u8 playlist...

this is without a key and works well...

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:2061084
#EXTINF:9.009,
26_1_2061084.ts?m=1492714001
#EXTINF:9.009,
26_1_2061085.ts?m=1492714001
#EXTINF:9.009,
26_1_2061086.ts?m=1492714001
#EXTINF:9.009,
26_1_2061087.ts?m=1492714001
#EXTINF:9.009,
26_1_2061088.ts?m=1492714001
#EXTINF:9.009,
26_1_2061089.ts?m=1492714001

and this is the one with a AES Key

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:2078799
#EXT-X-KEY:METHOD=AES-128,URI="mykey"
#EXTINF:9.009,
248_1_2078799.ts?m=1492793899
#EXTINF:9.009,
248_1_2078800.ts?m=1492793899
#EXTINF:9.009,
248_1_2078801.ts?m=1492793899
#EXT-X-KEY:METHOD=AES-128,URI="mykey2aftersegment"
#EXTINF:9.009,
248_1_2078802.ts?m=1492793899
#EXTINF:9.009,
248_1_2078803.ts?m=1492793899
#EXTINF:9.009,
248_1_2078804.ts?m=1492793899

So the only thing, i want the player to recognize that there's a key and play the segment. There's no ecryption on the key its already decrypt but it's rotating every segment. That's my issue!

jboisjo commented 7 years ago

Hey Brian,

sorry to bug you again, I think i found a way to add headers thru HttpDataSource with setRequestProperty. Now my question, How i could add a new Datasource in my override method?

 @Override
    protected UriDataSource createManifestDataSource(Context context, String userAgent) {
        // TODO: Provide your authenticated data source
        return new DefaultUriDataSource(context, userAgent);
    }

Thank you very much for you help again! :)

brianwernick commented 7 years ago

I'm not sure what you mean, you would just return your modified DataSource from the createDataSource() method for media playback or createManifestDataSource() for manifest requests

jboisjo commented 7 years ago

Brian! i didnt want to open a new issue for that but i finally realize that i needed a session cookie to access to the manifest...:( my bad for all that! Could you give me a way to add the session cookie the player?

Thank you so much!!

brianwernick commented 7 years ago

The session cookie needs to be provided with the DataSource. If you are using an OkHttpClient (e.g. App#L69) then you can add the cookie there

jboisjo commented 7 years ago

I'm so sorry, misunderstanding for what i shouldve have done in the first place, i need to pass a String header which are the String agent and also a Token.

This is a working example in iOS

let headers = ["Authorization": "JWT \(liveToken)", "User-Agent" : UserAgent]
                self.playerAsset = AVURLAsset.init(url: urlStream! as URL, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
                let item = AVPlayerItem(asset: self.playerAsset!)
                self.playerItems.append(item)

As you can see, i'm adding a String token and the UserAgent...

Now in your library i added the String agent like this

 @Override
        protected DataSource createDataSource(Context context, TransferListener transferListener, String userAgent) {
            // TODO: Provide your AES 128 decrypting data source

            String userAgentLive = System.getProperty("http.agent");

            return new DefaultUriDataSource(context, transferListener, userAgentLive, true);
        }

the String Agent is working, but i have no clue how i could add my String Token inside that...:(

Thank you so much again for all your help!

jboisjo commented 7 years ago

Found it!!!

@Override
        protected DataSource createDataSource(Context context, TransferListener transferListener, String userAgent) {
            // TODO: Provide your AES 128 decrypting data source

            String userAgentLive = System.getProperty("http.agent");

            DefaultHttpDataSource defaultHttpDataSource = new DefaultHttpDataSource(userAgentLive,null);
            defaultHttpDataSource.setRequestProperty("Authorization", "JWT " + streamAPI.getToken());

            return defaultHttpDataSource;
        }

But now, it keeps the lowest resolution :( It doesn't scale up..

jboisjo commented 7 years ago

Finally everything is working!!!!!!!!!!

For those you wants to add Headers into your encrypted playlist, just do like this :)

you add setRequestProperty with (name, value) of what you need!

@Override
       protected DataSource createDataSource(Context context, TransferListener transferListener, String userAgent) {

           DefaultHttpDataSource defaultHttpDataSource = new DefaultHttpDataSource(userAgent , null , transferListener);
           defaultHttpDataSource.setRequestProperty("Authorization", "JWT " + streamAPI.getToken());

           return defaultHttpDataSource;
       }
jboisjo commented 7 years ago

Brian!

One more question about this, i need to update my streamAPI.getToken() that you see above after certain time. How could i just update the header while the player is playing?

Thank you so much again for your time.