9

Background

I've succeeded uploading an audio file (3gp) into Google-Drive.

Now I want to be able to play the file within the app.

The Google Drive API only allows to get the input stream of the file that's stored there.

The problem

All MediaPlayer capabilities of inputs aren't available in my case, which is only InputSteam:

http://developer.android.com/reference/android/media/MediaPlayer.html#setDataSource(java.io.FileDescriptor)

I know I can save the file from the Google-Drive to the cache and play it, but I want to avoid the storage handling, and play the file on the fly.

What I've tried

I tried to search for this issue, and only found that it might be possible using AudioTrack (here). It might also be possible using new Jelly-Bean features (shown here, found from here), but I'm not sure as it's quite low level.

Sadly, using AudioTrack I got wrong sounds being played (noise).

I've also noticed that MediaPlayer has the option to set the dataSource to be MediaDataSource (here) , but not only I'm not sure how to use it, it also requires API 23 and above.

Of course, I tried using a url that is given in Google-Drive, but this is only used for other purposes and isn't being directed to the audio file, so it can't be used using MediaPlayer.

The question

Given an InputStream, is it possible to use AudioTrack or something else, to play an audio 3gp file ?

Is there maybe a support library solution for this?

Community
  • 1
  • 1
android developer
  • 114,585
  • 152
  • 739
  • 1,270

4 Answers4

5

If your minSdkVersion is 23 or higher, you can use setDataSource(MediaDataSource) and supply your own subclass of the abstract MediaDataSource class.

For older devices, you should be able to use a pipe created from ParcelFileDescriptor. You would have a thread that writes data to your end of the pipe, and pass the FileDescriptor (from getFileDescriptor()) for the player's end to setDataSource(FileDescriptor).

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • About MediaDataSource, that's one solution I've found, but I don't see any example of how to use it. Can you please show some code for both ways? – android developer Apr 25 '16 at 08:48
  • @androiddeveloper: I have never used `MediaDataSource`; I just ran into it the other day when helping somebody else. While I have not used a `ParcelFileDescriptor` pipe for media playback, I have used it for [media recording](https://github.com/commonsguy/cw-omnibus/blob/master/Media/AudioRecordStream/app/src/main/java/com/commonsware/android/audiorecstream/MainActivity.java), [serving documents](https://github.com/commonsguy/cw-omnibus/blob/master/Documents/Provider/app/src/main/java/com/commonsware/android/documents/provider/DemoDocumentsProvider.java), etc. – CommonsWare Apr 25 '16 at 11:02
  • That's too bad. Thank you anyway. – android developer Apr 25 '16 at 14:59
  • @CommonsWare I have checked `setDataSource(FileDescriptor)`. It produces `W/System.err: java.io.IOException: setDataSourceFD failed.: status=0x80000000 W/System.err: at android.media.MediaPlayer._setDataSource(Native Method) W/System.err: at android.media.MediaPlayer.setDataSource(MediaPlayer.java:1133) W/System.err: at android.media.MediaPlayer.setDataSource(MediaPlayer.java:1118)' if there is just an InputStream under FileDescriptor and no a real file on Android file system, Mark :( – isabsent Sep 08 '17 at 12:49
  • @isabsent: Media playback needs a file or a supported stream server protocol (HTTP, RTSP, etc.). It does not work well when it cannot rewind, and a stream on a pipe (e.g., `ParcelFileDescriptor.createPipe()`) cannot be rewound. – CommonsWare Sep 08 '17 at 12:52
  • So, the only way to go with old devices is an implementing HTTP server on the local host and play stream from it? – isabsent Sep 08 '17 at 12:58
  • @isabsent: AFAIK, yes. You might see if ExoPlayer offers other options. For example, it might do enough caching to get past the rewind limitation. – CommonsWare Sep 08 '17 at 13:44
2

The simplest MediaDataSource implementation example:

import android.media.MediaDataSource;
import android.os.Build;
import android.support.annotation.RequiresApi;

import java.io.IOException;
import java.io.InputStream;

@RequiresApi(api = Build.VERSION_CODES.M)
public class InputStreamMediaDataSource extends MediaDataSource {
    private InputStream is;
    private long streamLength = -1, lastReadEndPosition;

    public InputStreamMediaDataSource(InputStream is, long streamLength) {
        this.is = is;
        this.streamLength = streamLength;
        if (streamLength <= 0){
            try {
                this.streamLength = is.available(); //Correct value of InputStream#available() method not always supported by InputStream implementation!
            } catch (IOException e) {
                e.printStackTrace();
            }
        }                 
    }

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

    @Override
    public synchronized int readAt(long position, byte[] buffer, int offset, int size) throws IOException {
        if (position >= streamLength)
            return -1;

        if (position + size > streamLength)
            size -= (position + size) - streamLength;

        if (position < lastReadEndPosition) {
            is.close();
            lastReadEndPosition = 0;
            is = getNewCopyOfInputStreamSomeHow();//new FileInputStream(mediaFile) for example.
        }

        long skipped = is.skip(position - lastReadEndPosition);
        if (skipped == position - lastReadEndPosition) {
            int bytesRead = is.read(buffer, offset, size);
            lastReadEndPosition = position + bytesRead;
            return bytesRead;
        } else {
            return -1;
        }
    }

    @Override
    public synchronized long getSize() throws IOException {
        return streamLength;
    }
}

To use it with API >= 23 you have to provide streamLength value and if (when) MediaPlayer goes back - i.e. position < lastReadEndPosition, you have to know how to create a new copy of InputStream.

Usage example:

You have to create Activity, initialize MediaPlayer class (there are a lot of examples of a file playback) and place following code istead of old player.setDataSource("/path/to/media/file.3gp")

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    File file = new File("/path/to/media/file.3gp");//It is just an example! If you have real file on the phone memory, you don't need to wrap it to the InputStream to play it in MediaPlayer!
    player.setDataSource(new InputStreamMediaDataSource(new FileInputStream(file), file.length()));
} else
    player.setDataSource(this, mediaUri);

If your file is an object com.google.api.services.drive.model.File on Google Drive and com.google.api.services.drive.Drive drive you can get

InputStream is = drive.getRequestFactory().buildGetRequest(new GenericUrl(file.getDownloadUrl())).execute().getContent();

In the case of Build.VERSION.SDK_INT < Build.VERSION_CODES.MI had to setup HTTP sever on the local host of an Android device (by means of NanoHTTPd) and transfer a byte stream thru this server to MediaPlayer by uri - player.setDataSource(this, mediaUri)

isabsent
  • 3,683
  • 3
  • 25
  • 46
  • Can you please show how to use it, and what to do with API < 23 (which is still quite a lot of users) ? – android developer Sep 10 '17 at 14:15
  • Please show how to get the "mediaUri" to work with the InputStream given by Drive API. That's what the question was about... – android developer Sep 10 '17 at 18:10
  • There are more than 20 java classes in the old version of NanoHTTPd that have been patched for my purposes to set up a server... I have to think how to represent all this code. – isabsent Sep 11 '17 at 02:24
  • Isn't there a better way? See what was written here: https://stackoverflow.com/a/36829873/878126 . – android developer Sep 11 '17 at 05:24
  • Do you mean ExoPlayer or what? – isabsent Sep 11 '17 at 07:16
  • The link goes to an answer above, saying "For older devices, you should be able to use a pipe created from ParcelFileDescriptor. You would have a thread that writes data to your end of the pipe, and pass the FileDescriptor (from getFileDescriptor()) for the player's end to setDataSource(FileDescriptor). " – android developer Sep 11 '17 at 07:40
  • It is impossible with ParcelFile Descriptor. I have tried this way couple years ago. Take a look on our subsequent discussion with @CommonsWare in that answer. – isabsent Sep 11 '17 at 08:11
  • Oh I didn't notice it got new comments. So maybe it's possible with the "ExoPlayer" ? It's a library from Google, right? – android developer Sep 11 '17 at 08:26
  • 1
    I am not sure :( I have never seen in docs that ExoPlayer can take InputSream on its input. Did you read? I suppose, it is necessary to watch the source code to understand it. Furthermore, my requirement was to support API >= 14, but ExoPlayer supports only API >= 16. – isabsent Sep 11 '17 at 08:44
  • Sorry for that. I have left this thread a long time ago (so I stopped researching about it), as I didn't need this anymore. I will grant you +1 for the effort though. If you succeed showing a nice solution for pre API 23, even with using ExpPlayer, this could be very nice. – android developer Sep 11 '17 at 09:21
  • Thanks. I suppose the best solution at this moment is to use [Android VLC libraries](https://github.com/videolabs/vlc-android) into your app as a media player. It supports a fair stream playback, a lot of media files and plays well and stable as I can see - [VLC](https://play.google.com/store/apps/details?id=org.videolan.vlc) and [321](https://play.google.com/store/apps/details?id=my.bhul.video.player) – isabsent Sep 11 '17 at 10:01
  • Do you know of a sample of how to use it, for this use case? – android developer Sep 11 '17 at 13:14
  • 1
    You have to inplement ContentProvider in your app with method `ParcelFileDescriptor openFile(Uri uri, String mode)`. In this method you have to implement @CommonsWare scheme with `ParceFileDescriptor` and `InputStream` and send to external world intent with Uri `content://your.content.provider.name/...` which will identify your InputStream. VLC will catch this intent and ask Android to provide an app which has such InputStream (i.e. your app - by means of your `ContentProvider`). After it VLC will begin to pump out InputStream you have provided. – isabsent Sep 11 '17 at 13:50
  • Yes I understand the idea, but is there a sample app to test it? – android developer Sep 11 '17 at 14:16
  • 1
    I have no a sample app, it works as a part of [BestCrypt Explorer](https://play.google.com/store/apps/details?id=com.jetico.bestcrypt). But there is not a lot of code and I can write the code sample a little bit later. – isabsent Sep 11 '17 at 15:18
  • **@android developer:** I have prepared a code sample for you. Ask a new question on SO, let me know about it here and I will answer it. – isabsent Sep 13 '17 at 14:13
0

For anyone interested in using a MediaDataSource implementation I've created one that reads ahead and caches buffers of data. It works from any InputStream, I've mainly created it for reading from networked files using JCIFS SmbFile's.

You can find it at https://github.com/SteveGreatApe/BufferedMediaDataSource

greatape
  • 324
  • 3
  • 5
  • Whilst this may theoretically answer the question, [it would be preferable](//meta.stackoverflow.com/q/8259) to include the essential parts of the answer here, and provide the link for reference. See [here](https://meta.stackexchange.com/a/94027/285661) for instructions how to write *better* "link-based" answers. Thanks! – GhostCat Sep 08 '17 at 11:24
  • Nice. Thank you. – android developer Sep 10 '17 at 05:18
-1

If you asking about media player which set our audio path maybe this code can help.

File directory = Environment.getExternalStorageDirectory();
  File file = new File( directory + "/AudioRecorder" );
  String AudioSavePathInDevice = file.getAbsolutePath() + "/" + "sample.wav" ;
    
     mediaPlayer = new MediaPlayer();
     
      try {
            mediaPlayer.setDataSource(AudioSavePathInDevice);
            mediaPlayer.prepare();
          } catch (IOException e) {
              e.printStackTrace();
          }

      mediaPlayer.start();