androidx / media

Jetpack Media3 support libraries for media use cases, including ExoPlayer, an extensible media player for Android
https://developer.android.com/media/media3
Apache License 2.0
1.57k stars 373 forks source link

player.seekto() exception when playing encrypted video: Invalid NAL length #1643

Open ladeng opened 3 weeks ago

ladeng commented 3 weeks ago

I use encrypt with “AES/CBC/PKCS5Padding”

I am playing a local encrypted video. after setting the video resource, sliding the progress bar to set the playback progress(player.seekTo) will cause an exception: ExoPlayerImplInternal: Playback error androidx.media3.exoplayer.ExoPlaybackException: Source error at androidx.media3.exoplayer.ExoPlayerImplInternal.handleIoException(ExoPlayerImplInternal.java:736) at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:706) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:219) at android.os.HandlerThread.run(HandlerThread.java:67) Caused by: androidx.media3.common.ParserException: Invalid NAL length{contentIsMalformed=true, dataType=1} at androidx.media3.extractor.mp4.Mp4Extractor.readSample(Mp4Extractor.java:725) at androidx.media3.extractor.mp4.Mp4Extractor.read(Mp4Extractor.java:332) at androidx.media3.exoplayer.source.BundledExtractorsAdapter.read(BundledExtractorsAdapter.java:147) at androidx.media3.exoplayer.source.ProgressiveMediaPeriod$ExtractingLoadable.load(ProgressiveMediaPeriod.java:1082) at androidx.media3.exoplayer.upstream.Loader$LoadTask.run(Loader.java:421) 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:929)

when the encrypted video is played to the last frame, the output error: E/ExoPlayerImplInternal: Playback error androidx.media3.exoplayer.ExoPlaybackException: Source error at androidx.media3.exoplayer.ExoPlayerImplInternal.handleIoException(ExoPlayerImplInternal.java:736) at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:712) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:219) at android.os.HandlerThread.run(HandlerThread.java:67) Caused by: java.io.EOFException at androidx.media3.exoplayer.source.SampleDataQueue.sampleData(SampleDataQueue.java:186) at androidx.media3.exoplayer.source.SampleQueue.sampleData(SampleQueue.java:602) at androidx.media3.extractor.TrackOutput.sampleData(TrackOutput.java:161) at androidx.media3.extractor.mp4.Mp4Extractor.readSample(Mp4Extractor.java:755) at androidx.media3.extractor.mp4.Mp4Extractor.read(Mp4Extractor.java:332) at androidx.media3.exoplayer.source.BundledExtractorsAdapter.read(BundledExtractorsAdapter.java:147) at androidx.media3.exoplayer.source.ProgressiveMediaPeriod$ExtractingLoadable.load(ProgressiveMediaPeriod.java:1082) at androidx.media3.exoplayer.upstream.Loader$LoadTask.run(Loader.java:421) 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:929)

but after the video is decrypted and output as an mp4 file, it is normal to play it again, so my encrypted video file seems to be fine? after several days of trying, I have not found a solution yet. please help me!

ladeng commented 3 weeks ago

Add the SDK version: implementation 'androidx.media3:media3-exoplayer:1.4.0' implementation 'androidx.media3:media3-exoplayer-dash:1.4.0' implementation 'androidx.media3:media3-ui:1.4.0'

icbaker commented 3 weeks ago

Since you came here from https://github.com/google/ExoPlayer/issues/11229, i just want to check: are you using a custom DataSource implementation similar to that issue? If so, my suggestion is the same as that issue: ensure your DataSource is passing all of the DataSourceContractTest tests.

If not, please can you describe in more detail how you are configuring the decryption within your player?

ladeng commented 3 weeks ago

The test has passed using DataSourceContractTest, please see the screenshot: 2024-08-23 11 54 17

Below is all my code: EncryptedDataSourceTest.java: `@RunWith(AndroidJUnit4.class) public class EncryptedDataSourceTest extends DataSourceContractTest {

private static final String TEST_FILE_PATH = "/storage/emulated/0/a/v/abc.mp4";
@Override
protected DataSource createDataSource() throws Exception {
    Cipher cipher = AesPassUtils.getCipher(Cipher.DECRYPT_MODE,AesPassUtils.AES_KEY,AesPassUtils.AES_IV);
    return new AESDecryptionDataSource(new AESDecryptionDataSourceFactory(cipher).createDataSource(),cipher);
}

@Override
protected ImmutableList<TestResource> getTestResources() throws Exception {
    return null;
}

@Override
protected Uri getNotFoundUri() {
    return null;
}

@Test
public void testEncryptedFileCanBeRead() throws Exception {
    DataSource dataSource = createDataSource();
    dataSource.open(new DataSpec(Uri.parse(TEST_FILE_PATH)));
    byte[] buffer = new byte[1024];
    int bytesRead = dataSource.read(buffer, 0, buffer.length);
    assertTrue("The encrypted file should be readable after decryption", bytesRead > 0);
    dataSource.close();
}

@Test
public void testPlayDecryptedVideo() throws Exception {
    Context context = ApplicationProvider.getApplicationContext();
    ExoPlayer[] player = new ExoPlayer[1];
    InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
        player[0] = new ExoPlayer.Builder(context).build();
    });

    DataSource.Factory dataSourceFactory = new DataSource.Factory() {
        @Override
        public DataSource createDataSource() {
            try {
                return EncryptedDataSourceTest.this.createDataSource();
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    };

    MediaSource mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory)
            .createMediaSource(MediaItem.fromUri(Uri.parse(TEST_FILE_PATH)));

    // main thread
    InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
        player[0].setMediaSource(mediaSource);
        player[0].prepare();
        player[0].play();
    });

    Thread.sleep(5000);

    InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
        System.out.println(String.format("ExoPlayer status:%d",player[0].getPlaybackState()));
        player[0].release();
    });
}

}`

AESDecryptionDataSource.java: `public class AESDecryptionDataSource implements DataSource {

private final DataSource upstream;
private final Cipher cipher;
private InputStream inputStream;
private long bytesRemaining;

public AESDecryptionDataSource(DataSource upstream, Cipher cipher) {
    this.upstream = upstream;
    this.cipher = cipher;
}

@Override
public void addTransferListener(TransferListener transferListener) {
    upstream.addTransferListener(transferListener);
}

@Override
public long open(DataSpec dataSpec) throws IOException {
    long dataSpecLength = upstream.open(dataSpec);
    bytesRemaining = dataSpecLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : dataSpecLength;

    inputStream = new CipherInputStream(new DataSourceInputStream(upstream, dataSpec), cipher);
    return dataSpecLength;
}

@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
    if (bytesRemaining == 0) {
        return C.RESULT_END_OF_INPUT;
    }

    int bytesRead = inputStream.read(buffer, offset, readLength);
    if (bytesRead == -1) {
        return C.RESULT_END_OF_INPUT;
    }

    if (bytesRemaining != C.LENGTH_UNSET) {
        bytesRemaining -= bytesRead;
    }

    return bytesRead;
}

@Override
public Uri getUri() {
    return upstream.getUri();
}

@Override
public Map<String, List<String>> getResponseHeaders() {
    return DataSource.super.getResponseHeaders();
}

@Override
public void close() throws IOException {
    try {
        if (inputStream != null) {
            inputStream.close();
        }
    } finally {
        upstream.close();
    }
}

private static class DataSourceInputStream extends InputStream {
    private final DataSource dataSource;
    private final DataSpec dataSpec;
    private boolean opened = false;

    DataSourceInputStream(DataSource dataSource, DataSpec dataSpec) {
        this.dataSource = dataSource;
        this.dataSpec = dataSpec;
    }

    @Override
    public int read() throws IOException {
        byte[] singleByte = new byte[1];
        int result = read(singleByte);
        return result == -1 ? -1 : (singleByte[0] & 0xFF);
    }

    @Override
    public int read(byte[] buffer, int offset, int readLength) throws IOException {
        if (!opened) {
            dataSource.open(dataSpec);
            opened = true;
        }
        return dataSource.read(buffer, offset, readLength);
    }

    @Override
    public void close() throws IOException {
        dataSource.close();
    }
}

}`

AESDecryptionDataSourceFactory.java: `public class AESDecryptionDataSourceFactory implements DataSource.Factory { private final DataSource.Factory fileDataSourceFactory; private final Cipher cipher;

public AESDecryptionDataSourceFactory(Cipher cipher) { this.cipher = cipher; this.fileDataSourceFactory = new FileDataSource.Factory(); }

@Override public DataSource createDataSource() { return new AESDecryptionDataSource(fileDataSourceFactory.createDataSource(), cipher); } }`

hopefully this information can help resolve the issue

icbaker commented 2 weeks ago

Something isn't right in your screenshot. I would expect to see all the tests defined in DataSourceContractTest being run, like when I run FileDataSourceContractTest:

Screenshot 2024-08-27 at 17 45 50

In your screenshot it seems that none of these inherited tests are being run.

I would also expect to most of these tests failing with a NullPointerException because you can't return null from getTestResources() or getNotFoundUri().


I would take a look at FileDataSourceContractTest and work on fixing your test set-up so that the contract tests are running.

icbaker commented 2 weeks ago

You may also want to take a look at https://github.com/androidx/media/issues/856.

From your original comment you mention:

I use encrypt with “AES/CBC/PKCS5Padding”

Since you are using CBC then you likely need similar code to that in https://github.com/androidx/media/issues/856#issuecomment-2308585356 which adjusts the read position of the upstream encrypted datasource to ensure it always starts from the beginning of a block. Otherwise you will often start decrypting from the middle of a block, which I think will produce garbage decrypted data, and likely explains the exception you're seeing when seeking (when we open the data source at a byte offset based on the seek position).

markg85 commented 2 weeks ago

@ladeng Reading can't be that hard, right? I quote myself from https://github.com/androidx/media/issues/856

image

If you want the tests to work then you need to skip that part of the diff. The compile will fail for various reasons which you will all have to fix appropriately.

vallemar commented 2 days ago

+1, same error here