ExoPlayer icon indicating copy to clipboard operation
ExoPlayer copied to clipboard

Can not stream an encrypted hls video

Open InfinityLoop1308 opened this issue 3 years ago • 5 comments

ExoPlayer Version

2.17.1

Devices that reproduce the issue

NA

Devices that do not reproduce the issue

NA

Reproducible in the demo app?

Not tested

Reproduction steps

I can not provide the url since it will expire in a few minutes, but I can provide the final .ts file tmp. When I debug it, it seems the HlsMediaChunk.java:495 will execute long bytesToRead = dataSource.open(dataSpec); , which will finally call Aes128dataSource.open(), and it seems the only available return value for this funtion is return C.LENGTH_UNSET; so that bytesToRead will be -1 and the streaming procedure can not process.

Please tell me where is the mistake, thanks

Expected result

NA

Actual result

NA

Media

NA

Bug Report

  • [ ] You will email the zip file produced by adb bugreport to [email protected] after filing this issue.

InfinityLoop1308 avatar Jul 04 '22 02:07 InfinityLoop1308

Please share a playlist and a bug-report obtained after the issue occurs, otherwise it's very hard to assist.

Thanks

christosts avatar Jul 07 '22 15:07 christosts

Please share a playlist and a bug-report obtained after the issue occurs, otherwise it's very hard to assist.

Thanks

Hi, thanks for help.

The problem is, as I mentioned, the video will expire in about 2 minutes, and is geo-restricted to Japan.

This is the crash log

Crash log

com.google.android.exoplayer2.ExoPlaybackException: Source error
	at com.google.android.exoplayer2.ExoPlayerImplInternal.handleIoException(ExoPlayerImplInternal.java:641)
	at com.google.android.exoplayer2.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:611)
	at android.os.Handler.dispatchMessage(Handler.java:102)
	at android.os.Looper.loop(Looper.java:223)
	at android.os.HandlerThread.run(HandlerThread.java:67)
Caused by: com.google.android.exoplayer2.ParserException: Cannot find sync byte. Most likely not a Transport Stream.
	at com.google.android.exoplayer2.extractor.ts.TsExtractor.findEndOfFirstTsPacketInBuffer(TsExtractor.java:460)
	at com.google.android.exoplayer2.extractor.ts.TsExtractor.read(TsExtractor.java:327)
	at com.google.android.exoplayer2.source.hls.BundledHlsMediaChunkExtractor.read(BundledHlsMediaChunkExtractor.java:67)
	at com.google.android.exoplayer2.source.hls.HlsMediaChunk.feedDataToExtractor(HlsMediaChunk.java:473)
	at com.google.android.exoplayer2.source.hls.HlsMediaChunk.loadMedia(HlsMediaChunk.java:437)
	at com.google.android.exoplayer2.source.hls.HlsMediaChunk.load(HlsMediaChunk.java:394)
	at com.google.android.exoplayer2.upstream.Loader$LoadTask.run(Loader.java:412)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
	at java.lang.Thread.run(Thread.java:923)

Hope this helped. If you need any additional information, please reply me.

InfinityLoop1308 avatar Jul 07 '22 22:07 InfinityLoop1308

In the issue description you mention Aes128dataSource, so I assume HLS segments are encrypted with AES-128. My starting point of investigation would be to see if the HLS playlist is formed correctly and that the player parses the encryption information correctly to decrypt the segments. So we need to start at least with the playlist.

We cannot assist further without having some testing data. We can get access to geo-restricted content via vpn.

christosts avatar Jul 08 '22 12:07 christosts

In the issue description you mention Aes128dataSource, so I assume HLS segments are encrypted with AES-128. My starting point of investigation would be to see if the HLS playlist is formed correctly and that the player parses the encryption information correctly to decrypt the segments. So we need to start at least with the playlist.

We cannot assist further without having some testing data. We can get access to geo-restricted content via vpn.

Hi. It is a relatively complicated procedure, so I will post my code. Jsoup and nanoJson are used. First you need to get the download url. The function is provided below, and the input url should be https://www.nicovideo.jp/watch/so35380413

public String getNicoUrl(String url){
        final Map<String, List<String>> headers = new HashMap<>();
        headers.put("Content-Type", Collections.singletonList("application/json"));
        DownloaderImpl downloader = DownloaderImpl.getInstance(); //a simple okhttp downloader and can be replaced
        Response response;
        try {
            response = downloader.get(String.valueOf(url), null, NiconicoService.LOCALE); // NiconicoService.LOCALE = Localization.fromLocalizationCode("ja-JP")
            final Document page = Jsoup.parse(response.responseBody());
            JsonObject watch = JsonParser.object().from(
                    page.getElementById("js-initial-watch-data").attr("data-api-data"));
            final JsonObject session
                    = watch.getObject("media").getObject("delivery").getObject("movie");

            final JsonObject encryption = watch.getObject("media").getObject("delivery").getObject("encryption");
            final String s = NiconicoDMCPayloadBuilder.buildJSON(session.getObject("session"), encryption);
            response = downloader.post("https://api.dmc.nico/api/sessions?_format=json", headers, s.getBytes(StandardCharsets.UTF_8), NiconicoService.LOCALE);
            final JsonObject content = JsonParser.object().from(response.responseBody());
            final String contentURL = content.getObject("data").getObject("session")
                    .getString("content_uri");
            return String.valueOf(Uri.parse(contentURL));
        } catch (ReCaptchaException | JsonParserException | IOException e) {
            e.printStackTrace();
        }
        return null;
    }
// NiconicoDMCPayloadBuilder.buildJSON
   public static String buildJSON(final JsonObject obj, final JsonObject encryption) throws JsonParserException {
        JsonStringWriter temp = JsonWriter.string()
                .object()
                .object("session")
                .value("recipe_id", obj.getString("recipeId"))
                .value("content_id", obj.getString("contentId"))
                .value("content_type", "movie")
                .array("content_src_id_sets")
                .object()
                .array("content_src_ids")
                .object()
                .object("src_id_to_mux")
                .array("video_src_ids", obj.getArray("videos"))
                .array("audio_src_ids", obj.getArray("audios"))
                .end()
                .end()
                .end()
                .end()
                .end()
                .value("timing_constraint", "unlimited")
                .object("keep_method")
                .object("heartbeat")
                .value("lifetime", obj.getLong("heartbeatLifetime"))
                .end()
                .end()
                .object("protocol")
                .value("name", "http")
                .object("parameters")
                .object("http_parameters")
                .object("parameters")
                .object(obj.getArray("protocols").getString(0).equals("hls") ? "hls_parameters" : "http_output_download_parameters")
                .value("use_well_known_port", "yes")
                .value("use_ssl", "yes")
                .value("transfer_preset", "")
                .value("segment_duration", 6000);
        JsonObject parsedToken = JsonParser.object().from(obj.getString("token"));
        if(parsedToken.containsKey("hls_encryption") && encryption != null){
            temp = temp.object("encryption")
                    .object(parsedToken.getString("hls_encryption"))
                    .value("encrypted_key", encryption.getString("encryptedKey"))
                    .value("key_uri", encryption.getString("keyUri"))
                    .end().end();
        }
        return  temp
                .end()
                .end()
                .end()
                .end()
                .end()
                .value("content_uri", "")
                .object("session_operation_auth")
                .object("session_operation_auth_by_signature")
                .value("token", obj.getString("token"))
                .value("signature", obj.getString("signature"))
                .end()
                .end()
                .object("content_auth")
                .value("auth_type",
                        obj.getObject("authTypes").getString(obj.getArray("protocols")
                                .getString(0)))
                .value("content_key_timeout", obj.getLong("contentKeyTimeout"))
                .value("service_id", "nicovideo")
                .value("service_user_id", obj.getString("serviceUserId"))
                .end()
                .object("client_info")
                .value("player_id", obj.getString("playerId"))
                .end()
                .value("priority", obj.getDouble("priority"))
                .end()
                .end()
                .done();
    }

After obtain the download url, you need to configure the header to avoid being blocked.

.setDefaultRequestProperties(Map.of("Referer", "https://www.nicovideo.jp/",
                        "Origin", "https://www.nicovideo.jp",
                        "X-Frontend-ID", "6",
                        "X-Frontend-Version", "0",
                        "X-Niconico-Language", "en-us"
                )))

Hope these help.

InfinityLoop1308 avatar Jul 08 '22 12:07 InfinityLoop1308

Hi. Any updates here? If more info is needed, please feel free to ask me.

InfinityLoop1308 avatar Jul 23 '22 06:07 InfinityLoop1308

Hi, I know it is a torture to debug with such long codes. To process it, I would like to debug myself and provide you the result, and you can just tell me how to do it. Best regards.

InfinityLoop1308 avatar Aug 16 '22 10:08 InfinityLoop1308

Hi! I made some further investigation. It turns out that exoplayer successfully getting all contents(i.e. master.m3u8, playlist.m3u8, license, 1.ts). But after getting the ts file, it fails to be parsed. I have uploaded all the 4 files there, and please help me check it. Thanks! @christosts

InfinityLoop1308 avatar Jan 07 '23 01:01 InfinityLoop1308

any updates?

InfinityLoop1308 avatar Jan 20 '23 00:01 InfinityLoop1308

Could you please check this? @christosts Or maybe I should open a new issue?

InfinityLoop1308 avatar Jan 14 '24 09:01 InfinityLoop1308

We don't need it anymore. Will close it.

InfinityLoop1308 avatar Feb 28 '24 11:02 InfinityLoop1308