20

Related to my recent question on MediaRecorder and createPipe(), and a discussion of the createPipe() technique in this other SO question, I am now trying to get MediaPlayer to work with content served by a ContentProvider via ParcelFileDescriptor and createPipe().

This sample project has my work to date. It is based off of an earlier sample that plays an OGG clip stored as a raw resource. Hence, I know that my clip is fine.

I have changed my MediaPlayer setup to:

  private void loadClip() {
    try {
      mp=new MediaPlayer();
      mp.setDataSource(this,
                       PipeProvider.CONTENT_URI.buildUpon()
                                               .appendPath("clip.ogg")
                                               .build());
      mp.setOnCompletionListener(this);
      mp.prepare();
    }
    catch (Exception e) {
      goBlooey(e);
    }
  }

Through logging in PipeProvider, I see that my Uri is being properly constructed.

PipeProvider is the same one as in this sample project, which works for serving PDFs to Adobe Reader, which limits how screwed up my code can be. :-)

Specifically, openFile() creates a pipe from ParcelFileDescriptor:

  @Override
  public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    ParcelFileDescriptor[] pipe=null;

    try {
      pipe=ParcelFileDescriptor.createPipe();
      AssetManager assets=getContext().getResources().getAssets();

      new TransferTask(assets.open(uri.getLastPathSegment()),
                       new AutoCloseOutputStream(pipe[1])).start();
    }
    catch (IOException e) {
      Log.e(getClass().getSimpleName(), "Exception opening pipe", e);
      throw new FileNotFoundException("Could not open pipe for: "
          + uri.toString());
    }

    return(pipe[0]);
  }

where the background thread does a typical stream-to-stream copy:

  static class TransferTask extends Thread {
    InputStream in;
    OutputStream out;

    TransferTask(InputStream in, OutputStream out) {
      this.in=in;
      this.out=out;
    }

    @Override
    public void run() {
      byte[] buf=new byte[1024];
      int len;

      try {
        while ((len=in.read(buf)) > 0) {
          out.write(buf, 0, len);
        }

        in.close();
        out.close();
      }
      catch (IOException e) {
        Log.e(getClass().getSimpleName(),
              "Exception transferring file", e);
      }
    }
  }

However, MediaPlayer chokes:

10-16 13:33:13.203: E/MediaPlayer(3060): Unable to to create media player
10-16 13:33:13.203: D/MediaPlayer(3060): Couldn't open file on client side, trying server side
10-16 13:33:13.207: E/TransferTask(3060): Exception transferring file
10-16 13:33:13.207: E/TransferTask(3060): java.io.IOException: write failed: EPIPE (Broken pipe)
10-16 13:33:13.207: E/TransferTask(3060):   at libcore.io.IoBridge.write(IoBridge.java:462)
10-16 13:33:13.207: E/TransferTask(3060):   at java.io.FileOutputStream.write(FileOutputStream.java:187)
10-16 13:33:13.207: E/TransferTask(3060):   at com.commonsware.android.audiolstream.PipeProvider$TransferTask.run(PipeProvider.java:120)
10-16 13:33:13.207: E/TransferTask(3060): Caused by: libcore.io.ErrnoException: write failed: EPIPE (Broken pipe)
10-16 13:33:13.207: E/TransferTask(3060):   at libcore.io.Posix.writeBytes(Native Method)
10-16 13:33:13.207: E/TransferTask(3060):   at libcore.io.Posix.write(Posix.java:178)
10-16 13:33:13.207: E/TransferTask(3060):   at libcore.io.BlockGuardOs.write(BlockGuardOs.java:191)
10-16 13:33:13.207: E/TransferTask(3060):   at libcore.io.IoBridge.write(IoBridge.java:457)
10-16 13:33:13.207: E/TransferTask(3060):   ... 2 more
10-16 13:33:13.211: E/MediaPlayer(3060): Unable to to create media player
10-16 13:33:13.218: E/TransferTask(3060): Exception transferring file
10-16 13:33:13.218: E/TransferTask(3060): java.io.IOException: write failed: EPIPE (Broken pipe)
10-16 13:33:13.218: E/TransferTask(3060):   at libcore.io.IoBridge.write(IoBridge.java:462)
10-16 13:33:13.218: E/TransferTask(3060):   at java.io.FileOutputStream.write(FileOutputStream.java:187)
10-16 13:33:13.218: E/TransferTask(3060):   at com.commonsware.android.audiolstream.PipeProvider$TransferTask.run(PipeProvider.java:120)
10-16 13:33:13.218: E/TransferTask(3060): Caused by: libcore.io.ErrnoException: write failed: EPIPE (Broken pipe)
10-16 13:33:13.218: E/TransferTask(3060):   at libcore.io.Posix.writeBytes(Native Method)
10-16 13:33:13.218: E/TransferTask(3060):   at libcore.io.Posix.write(Posix.java:178)
10-16 13:33:13.218: E/TransferTask(3060):   at libcore.io.BlockGuardOs.write(BlockGuardOs.java:191)
10-16 13:33:13.218: E/TransferTask(3060):   at libcore.io.IoBridge.write(IoBridge.java:457)
10-16 13:33:13.218: E/TransferTask(3060):   ... 2 more

Has anyone seen working code for using createPipe() to serve media to MediaPlayer?

Thanks in advance!

Community
  • 1
  • 1
CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491

4 Answers4

12

I'm not sure this can ever work. When I run this code I see this trace:

I/AudioSystem(30916): getting audio flinger
I/AudioSystem(30916): returning new audio session id
D/IAudioFlinger(30916): newAudioSessionId In
D/AudioFlinger(28138): nextUniqueId, current 178
D/IAudioFlinger(30916): newAudioSessionId Out, id = 178
D/MediaPlayer(30916): setDataSource(Context context, content://com.commonsware.android.audiolstream/clip.ogg, Map<String, String> headers) in
D/MediaPlayer(30916): setDataSource(FileDescriptor fd) in
E/MediaPlayerService(28138): offset error

That "offset error" comes from the following lines in MediaPlayerService.cpp in AOSP, where it does a fstat() on the read side of the pipe:

status_t MediaPlayerService::Client::setDataSource(int fd, int64_t offset, int64_t length)
{
    struct stat sb;
    int ret = fstat(fd, &sb);

    ....

    if (offset >= sb.st_size) {
        LOGE("offset error");
        ::close(fd);
        return UNKNOWN_ERROR;
    }

And sb.st_size is reported as -1 (via getStatSize() on the ParcelFileDescriptor at the Java level). The error handler closes the descriptor, hence the broken pipe error shortly afterwards.

In my experience MediaPlayer has many broken bits like this. I've never seen it work for anything but directly on local files, and (very buggily) for HTTP streaming. I ended up porting FFmpeg to work around its numerous failings.

Reuben Scratton
  • 38,595
  • 9
  • 77
  • 86
  • 1
    "And sb.st_size is reported as -1 (via getStatSize() on the ParcelFileDescriptor at the Java level)" -- well, that would seem to be where the problem lies, at least in part. There should be some way that we could say that this pipe will transmit X bytes, so `getStatSize()` could return a valid value, at least for cases where we know up front the eventual size. But, since this object is passed across process boundaries, we cannot really override `getStatSize()` ourselves. Phooey. Thanks for the insights! – CommonsWare Oct 17 '12 at 14:50
  • 1
    IMHO MediaPlayer shouldn't be using fstat at all, it should just read from the descriptor its given and not make assumptions about what kind of descriptor it is. – Reuben Scratton Oct 18 '12 at 09:29
8

I've tried to use pipes with MediaPlayer via a ContentProvider using PipeDataWriter (which basically uses a pipe and a thread).

The problem is that the file descriptor expected by the MediaPlayer, at least for video content, must be seekable, and you cannot do a fseek on a pipe.

luciofm
  • 737
  • 5
  • 14
  • 2
    Ah, very interesting. This certainly seems like a plausible explanation. I'll keep this question open for a bit to see if somebody has a workaround. Thanks! – CommonsWare Oct 17 '12 at 13:37
  • I'm still trying to play a cryptographed video with MediaPlayer, without having to write my own video player and decoding... – luciofm Oct 17 '12 at 14:30
  • I am under the impression that local HTTP streaming works, though I have not tried it. This `createPipe()` approach was to see if you could implement streaming playback without opening a socket. – CommonsWare Oct 17 '12 at 14:34
  • 1
    I have tried HTTP streaming from a background thread into MediaPlayer, and yes it does work but it can be very flaky. It drops the connection randomly (optimistic timeouts perhaps). – Reuben Scratton Oct 17 '12 at 14:50
  • I've done something similar for HTTPS streaming support for Android 2.3 and 2.2... I've create a small C daemon using openssl to proxy the HTTPS connection through a local open connection... – luciofm Oct 17 '12 at 16:49
  • http is slow but works for audio, when video and I use players with seekable stream it doesnt bother to work even with external players like VLC or MX – duckduckgo Sep 16 '13 at 11:43
  • 1
    The answer is correct. The function header, at least for android-28, clearly states "The FileDescriptor must be seekable (N.B. a LocalSocket is not seekable)." How to get there: In Android Studio select the setDataSource() call in your code and press Cmd-B. Note that nearly all parameter variants internally call the low-level-file-descriptor-variant, and this suffers from that limitation. – Andreas K. aus M. Apr 25 '20 at 10:42
1
From Api level 23 onwards, you can use MediaDataSource class.


import java.io.*;
import android.media.MediaDataSource;

public class MyAudioSource extends MediaDataSource {
    private final byte[] buf;
    private final ByteArrayInputStream is;

    public MyAudioSource(byte[] buf){
        super();
        this.buf=buf;
        is=new ByteArrayInputStream(buf);
    }

    public long getSize() {
        return buf.length;
    }

    public int readAt(long position, byte[] buffer, int offset, int size){
        is.reset();
        is.skip(position);
        return is.read(buffer,offset,size);
    }
}



Now use above class for MediaPlayer like:

    // some how get your audio buffer in buf
    MyAudioSource mas = new MyAudioSource(buf);
    mediaPlayer.setDataSource(mas);
  • 1
    `readAt()` requires you to be able to read from an arbitrary `position` in the data stream. Since `createPipe()` results in a non-seekable stream, we cannot just have `readAt()` call something on the pipe to read those bytes. A client might do its own buffering of bytes read off of the pipe, to be able to satisfy `readAt()` calls for previous and current positions. But the client would need to know that all of this is required; the focus of my question was getting a `createPipe()`-defined stream working without client-side workarounds. But thanks for pointing this out! – CommonsWare Oct 09 '18 at 11:56
  • I agree with you that my answer is not solving Pipe-related issue. I too wanted source MediaPlayer from a buffer to play a small mp3 files. Google eventually pointed to Stackoverflow. To circumvent this Pipe and seek related issues, I just browsed thru the MediaPlayer API and found this MediaDataSource method. – mahadevan gss Oct 09 '18 at 12:07
0

In thesis, openAssetFile() on your ContentProvider can be overriden. An AssetFileDescriptor can be returned with declared size and offset.

@Override
public AssetFileDescriptor openAssetFile(Uri uri, String mode)
        throws FileNotFoundException {

    ParcelFileDescriptor fd = openFile(uri, mode);
    return fd != null ? new AssetFileDescriptor(fd, offset, size) : null;

}

This values are passed to native setDataSource() on MediaPlayer (check the MediaPlayer.java to learn more).

If the error check in MediaPlayerService.cpp is (offset >= sb.st_size), an offset minor then -1 (the presumed size of content) or a positive declared size doesn't trigger the error.

This should be a good starting point to a clean hack, but i have bad luck on my tests. The dumb MediaPlayer seems read the whole "file" before play, causing a broken pipe ahead.

MobileCushion
  • 7,065
  • 7
  • 42
  • 62
Renascienza
  • 1,647
  • 1
  • 13
  • 16