Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

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

Closed
ladeng opened this issue Aug 22, 2024 · 8 comments
Closed
Assignees
Labels

Comments

@ladeng
Copy link

ladeng commented Aug 22, 2024

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
Copy link
Author

ladeng commented Aug 22, 2024

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 icbaker self-assigned this Aug 22, 2024
@icbaker
Copy link
Collaborator

icbaker commented Aug 22, 2024

Since you came here from google/ExoPlayer#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
Copy link
Author

ladeng commented Aug 23, 2024

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
Copy link
Collaborator

icbaker commented Aug 27, 2024

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
Copy link
Collaborator

icbaker commented Aug 27, 2024

You may also want to take a look at #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 #856 (comment) 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
Copy link

markg85 commented Aug 27, 2024

@ladeng Reading can't be that hard, right? I quote myself from #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
Copy link

+1, same error here

@icbaker
Copy link
Collaborator

icbaker commented Sep 25, 2024

I'm going to close this because I think the original questions have been answered, and additional high-touch support on the details of implementing a DataSource correctly are beyond the capacity we have on this issue tracker. If you have more questions, please file a new issue with details of a passing run of a DataSourceContractTest (see my comment above for what this should look like: #1643 (comment)).

@icbaker icbaker closed this as completed Sep 25, 2024
@androidx androidx locked and limited conversation to collaborators Nov 25, 2024
# for free to subscribe to this conversation on GitHub. Already have an account? #.
Labels
Projects
None yet
Development

No branches or pull requests

5 participants