0

I have a Xamarin application which plays remote(internet) audio files using the MediaPlayer with the following setup:

_mediaPlayer.SetDataSource(mediaUri);
_mediaPlayer.PrepareAsync();

Now I would like to change the implementation to also cache files. For the caching part I found a really nice library called MonkeyCache which saves the files in this JSON format:

{"$type":"System.Byte[], mscorlib","$value":"UklGRhAoAgBXQVZFZm10IBAAAAABAAIARKwAABCxAgAEABAAZGF0YdAnAgAAAAAAAAAAAAAAAAAA+P/4/+z/7P8KAAoA7//v//L/8v8JAAkA6f/p//j/+P8FAAUA6P/o/wEAAQD+//7/5//n ................."}

So my MediaPlayer setup has now changed to:

if (Barrel.Current.Exists(mediaUri)){
       var audio = Barrel.Current.Get<byte[]>(mediaUri);
       _mediaPlayer.SetDataSource(???);
}else{
       using (var webClient = new WebClient()){
            var downloadDataBytes = webClient.DownloadData(mediaUri);
            if (downloadDataBytes != null && downloadDataBytes.Length > 0)
            {
               Barrel.Current.Add(mediaUri, downloadDataBytes, TimeSpan.FromDays(1));
               _mediaPlayer.SetDataSource(???);
             }
        }
 }

I would like to play the audio from a byte[] instead of the mediaUri. Is there any way to actually play an in memory byte[]?

The only solutions that I could find were to create a FileInputStream out of a File by using a filepath, but the implementation of the MonkeyCache actually hashes the file name, before adding it:

static string Hash(string input){
    var md5Hasher = MD5.Create();
    var data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(input));
            return BitConverter.ToString(data);
}

Therefore the downloaded bytes, will be saved under:

/data/data/com.package.name.example/cache/com.package.name.example/MonkeyCacheFS/81-D6-E8-62-F3-4D-F1-64-A6-A1-53-46-34-1E-FE-D1

Even if I were to use the same hashing logic to actually compute the file path myself and use the FileInputStream which might be working by what I've read, it would defeat the purpose of using the var audio = Barrel.Current.Get<byte[]>(mediaUri); functionality of the MonkeyCache.

However, if that is the only way, I will do it.

Edit: Even with my described approach, it would probably not work right away as even if I compute the right file name, it is still in JSON format.

Edit2: A working solution is:

var audio = Barrel.Current.Get<byte[]>(mediaUri);
var url = "data:audio/mp3;base64," + Convert.ToBase64String(audio);
_mediaPlayer.SetDataSource(url);
Rohit Singh
  • 16,950
  • 7
  • 90
  • 88
Bogdan Daniel
  • 2,689
  • 11
  • 43
  • 76

1 Answers1

0

Finally figured it out:

The MediaPlayer.SetDataSource method has an overload which expects a MediaDataSource object.

MediaDataSource
For supplying media data to the framework. Implement this if your app has special requirements for the way media data is obtained

Therefore, you can use your implementation of the MediaDataSource and feed it your byte array.

public class StreamMediaDataSource : MediaDataSource
    {
        private byte[] _data;

        public StreamMediaDataSource(byte[] data)
        {
            _data = data;
        }

        public override long Size
            => _data.Length;

        public override void Close()
        {
            _data = null;
        }

        public override int ReadAt(long position, byte[] buffer, int offset, int size)
        {

            if (position >= _data.Length)
            {
                return -1;
            }

            if (position + size > _data.Length)
            {
                size -= (Convert.ToInt32(position) + size) - _data.Length;
            }
            Array.Copy(_data, position, buffer, offset, size);
            return size;
        }
    }

which will lead to the MediaPlayer data source to be

_mediaPlayer.SetDataSource(new StreamMediaDataSource(downloadedDataBytes));

Edit: This only works for API >= 23. I still have to find a solution for API<23(which I still haven't).

Edit2: For API < 23 support I used the

var url = $"data:audio;base64,{Convert.ToBase64String(downloadedDataBytes)}";
_mediaPlayer.SetDataSource(url);

approach.

Bogdan Daniel
  • 2,689
  • 11
  • 43
  • 76
  • 1
    Watch out, you are going to run into issues if you intend playing mp4 files that have the header information written at the end - mediaplayer will not be able to do seek operations on the the base64 URI that gets generated! – gciochina Jan 18 '19 at 07:33
  • 1
    Wow. That is indeed correct. I just ran in this issue last night.I was looking for a nice solution. Probably I'll just have to get rid of MonkeyCache and cache files as they come without a JSON serializer over them. I've already tested with the answer provided here https://stackoverflow.com/questions/1972027/android-playing-mp3-from-byte and it should work if I stream directly from the file by using the file descriptor. – Bogdan Daniel Jan 18 '19 at 07:47