Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Playback from InputStream comes back [enhancement]. #4212

Closed
isabsent opened this issue May 6, 2018 · 8 comments
Closed

Playback from InputStream comes back [enhancement]. #4212

isabsent opened this issue May 6, 2018 · 8 comments
Assignees
Labels

Comments

@isabsent
Copy link

isabsent commented May 6, 2018

I have read #1086 and I have some arguments for the direct support of playback from InputStream.

If you need to play video file from an encrypted container there is no alternative for playback by means of InputStream since you can't copy this video file to disk before playback because it is a privacy breaking.

To implement a playback from InputStream for MediaPlayer, I have been forced to set up an HTTP-server on the local host of my device and send video file throw it to the HTTP interface of MediaPlayer. It was awful and looks very stupid but Google did not left us a choice.

...the InputStream interface doesn't provide suitable random access behavior for media playback, which is required both for seeking and because some media formats place data at the end of the file that must be read at the start of playback.

The simplest way to get a random acceess to the InputStream is to skip unwanted bytes, read wanted and recreate InputStream again for next reading.

It would be very useful if you provide an opportunity to play video from InputStream for such kind of applications!

@isabsent isabsent changed the title Playback from InputStream comes back. Playback from InputStream comes back [enhancement]. May 6, 2018
@ojw28
Copy link
Contributor

ojw28 commented May 6, 2018

The simplest way to get a random acceess to the InputStream is to skip unwanted bytes, read wanted and recreate InputStream again for next reading.

If this is what you want to do then you can implement an ExoPlayer DataSource that does this. You don't need direct support from the library to achieve your goal.

If you need to play video file from an encrypted container there is no alternative for playback by means of InputStream.

Whether this is true depends on what type of encryption you're using. If you use a type that supports random access, like AES/CTR/NoPadding, then you can use RandomAccessFile and decrypt on the fly as it's read. We actually support this mode directly with library components already, by making an AesCipherDataSource that wraps a regular FileDataSource. The wrapped FileDataSource reads using RandomAccessFile, and the wrapping AesCipherDataSource layer the on the fly decryption.

@ojw28 ojw28 self-assigned this May 6, 2018
@ojw28 ojw28 added the question label May 6, 2018
@isabsent
Copy link
Author

isabsent commented May 6, 2018

Thanks, I will try. Can you give me some example of ExoPlayer usage with an InputStream enveloped into appropriate DataSource (without encryption)?

@ojw28
Copy link
Contributor

ojw28 commented May 6, 2018

AssetDataSource is an example of a DataSource implementation that reads from an underlying InputStream, although what's different there is that we're able to make some assumptions about how the InputStream behaves beyond what's guaranteed by the InputStream interface (see comments in the class for details). You could use that class as a starting point though, remove the assumptions, and then add in decryption.

@isabsent
Copy link
Author

isabsent commented May 7, 2018

This is my custom InputStreamDataSource:

public class InputStreamDataSource implements DataSource {
    private InputStream inputStream;
    private long bytesRemaining;
    private boolean opened;

    @Override
    public long open(DataSpec dataSpec) throws IOException {
        try {
            File mediaFile = new File(Environment.getExternalStorageDirectory(), "song.mp3");
            inputStream = new FileInputStream(mediaFile);
            long skipped = inputStream.skip(dataSpec.position);
            if (skipped < dataSpec.position)
                throw new EOFException();

            if (dataSpec.length != C.LENGTH_UNSET) {
                bytesRemaining = dataSpec.length;
            } else {
                bytesRemaining = inputStream.available();
                if (bytesRemaining == Integer.MAX_VALUE)
                    bytesRemaining = C.LENGTH_UNSET;
            }
        } catch (IOException e) {
            throw new IOException(e);
        }

        opened = true;
        return bytesRemaining;
    }

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

        int bytesRead;
        try {
            int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
                    : (int) Math.min(bytesRemaining, readLength);
            bytesRead = inputStream.read(buffer, offset, bytesToRead);
        } catch (IOException e) {
            throw new IOException(e);
        }

        if (bytesRead == -1) {
            if (bytesRemaining != C.LENGTH_UNSET) {
                // End of stream reached having not read sufficient data.
                throw new IOException(new EOFException());
            }
            return C.RESULT_END_OF_INPUT;
        }
        if (bytesRemaining != C.LENGTH_UNSET) {
            bytesRemaining -= bytesRead;
        }
        return bytesRead;
    }

    @Override
    public Uri getUri() {
        // I can return any uri != null here and player will work! For example
        return Uri.fromFile(Environment.getExternalStorageDirectory());
    }

    @Override
    public void close() throws IOException {
        try {
            if (inputStream != null) {
                inputStream.close();
            }
        } catch (IOException e) {
            throw new IOException(e);
        } finally {
            inputStream = null;
            if (opened) {
                opened = false;
            }
        }
    }
}

and ExoPlayer initialization:

private void prepareExoPlayerFromInputStream(){
    exoPlayer = ExoPlayerFactory.newSimpleInstance(this, new DefaultTrackSelector(), new DefaultLoadControl());
    exoPlayer.addListener(eventListener);
    DataSpec dataSpec = new DataSpec(null);
    final InputStreamDataSource inputStreamDataSource = new InputStreamDataSource();
    try {
        inputStreamDataSource.open(dataSpec);
    } catch (IOException e) {
        e.printStackTrace();
    }

    DataSource.Factory factory = new DataSource.Factory() {

        @Override
        public DataSource createDataSource() {
            return inputStreamDataSource;
        }
    };
    MediaSource audioSource = new ExtractorMediaSource(inputStreamDataSource.getUri(),
            factory, new DefaultExtractorsFactory(), null, null);

    exoPlayer.prepare(audioSource);
    initMediaControls();
}

As you can see the getUri() returns the uri not related to my song.mp3 in above code but player still works fine. So, it seems, providing the uri != null is essential to get ExoPlayer working, but for which purposes ExoPlayer wants the uri - it is completely unclear. It shouldn't ask for Uri when playing from InputStream at all.

P.S.: ExoPlayer initialization code was adopted from this code sample.

P.P.S.: Something strange there - rewind definitely presents during playback, but read(byte[] buffer, int offset, int readLength) doesn't support it in this redaction! Does ExoPlayer have access to the file thru InputStream and rewind by means of it!?

@isabsent
Copy link
Author

isabsent commented May 7, 2018

Ok, I have found all answers by myself :) I suppose it will useful for others. The solution is:

public class InputStreamDataSource implements DataSource {
    private Context context;
    private DataSpec dataSpec;
    private InputStream inputStream;
    private long bytesRemaining;
    private boolean opened;

    public InputStreamDataSource(Context context, DataSpec dataSpec) {
        this.context = context;
        this.dataSpec = dataSpec;
    }

    @Override
    public long open(DataSpec dataSpec) throws IOException {
        try {
            inputStream = convertUriToInputStream(context, dataSpec.uri);
            long skipped = inputStream.skip(dataSpec.position);
            if (skipped < dataSpec.position)
                throw new EOFException();

            if (dataSpec.length != C.LENGTH_UNSET) {
                bytesRemaining = dataSpec.length;
            } else {
                bytesRemaining = inputStream.available();
                if (bytesRemaining == Integer.MAX_VALUE)
                    bytesRemaining = C.LENGTH_UNSET;
            }
        } catch (IOException e) {
            throw new IOException(e);
        }

        opened = true;
        return bytesRemaining;
    }

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

        int bytesRead;
        try {
            int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
                    : (int) Math.min(bytesRemaining, readLength);
            bytesRead = inputStream.read(buffer, offset, bytesToRead);
        } catch (IOException e) {
            throw new IOException(e);
        }

        if (bytesRead == -1) {
            if (bytesRemaining != C.LENGTH_UNSET) {
                // End of stream reached having not read sufficient data.
                throw new IOException(new EOFException());
            }
            return C.RESULT_END_OF_INPUT;
        }
        if (bytesRemaining != C.LENGTH_UNSET) {
            bytesRemaining -= bytesRead;
        }
        return bytesRead;
    }

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

    @Override
    public void close() throws IOException {
        try {
            if (inputStream != null) {
                inputStream.close();
            }
        } catch (IOException e) {
            throw new IOException(e);
        } finally {
            inputStream = null;
            if (opened) {
                opened = false;
            }
        }
    }

    private InputStream convertUriToInputStream(Context context, Uri mediaUri) {
        //Your implementation of obtaining InputStream from mediaUri
        return inputStream;
    }
}

Initialization of player (somewhere in the PlayerActivity):

private void prepareExoPlayerFromInputStream(Uri mediaUri){
    exoPlayer = ExoPlayerFactory.newSimpleInstance(this, new DefaultTrackSelector(), new DefaultLoadControl());
    exoPlayer.addListener(eventListener);

    DataSpec dataSpec = new DataSpec(mediaUri);
    final InputStreamDataSource inputStreamDataSource = new InputStreamDataSource(this, dataSpec);
    try {
        inputStreamDataSource.open(dataSpec);
    } catch (IOException e) {
        e.printStackTrace();
    }

    DataSource.Factory factory = new DataSource.Factory() {

        @Override
        public DataSource createDataSource() {
            return inputStreamDataSource;
        }
    };
    MediaSource audioSource = new ExtractorMediaSource(inputStreamDataSource.getUri(),
            factory, new DefaultExtractorsFactory(), null, null);

    exoPlayer.prepare(audioSource);
    initMediaControls();
}

Works fine with a real InputStream (I mean a case when there is no a file or byte[] or file descriptor underneath of InputStream. And rewind works fine too!

@ojw28
Copy link
Contributor

ojw28 commented May 7, 2018

Glad you've got something working. Just taking a quick look at your code: If you want this to work with any InputStream then you should be careful not to make assumptions that aren't guaranteed by the InputStream interface. For example your code is assuming InputStream.skip will skip the full requested amount, which is not guaranteed by the interface (but may be true for specific implementations). A more robust solution would be to read and discard data in a loop until you've skipped to the desired position.

@ojw28 ojw28 closed this as completed May 7, 2018
@isabsent
Copy link
Author

isabsent commented May 7, 2018

Thanks for clarification. InputStream properties and behaviour are under my control because it is produced by our native cryptographic libraries and will skip exactly the needed number of bytes.

As I understand getUri() is necessary to reopen InputStream if it has no random access to data (to rewind or find moov atom if it placed at the end of file)? Am I right?

@ojw28
Copy link
Contributor

ojw28 commented May 7, 2018

Random access to data is achieved by closing a DataSource and re-opening it at the desired position.

getUri() is used by consumers who need to know about redirects (i.e. when the actual content is being read from a Uri different to the one in the request that was passed to open(). I doubt this is relevant to your case; you should simply pass back the same Uri as was in the request (as is done in the AssetDataSource example I referred to earlier).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

2 participants