Wednesday, May 18, 2011

Using the Media Class in PhoneGap

I've seen a lot of posts recently on the PhoneGap Google Group so I wanted to put something together illustrating the use of the PhoneGap Media class. While putting this example together I noticed a few quirks of the Media class which I'll try in describe in more detail so that other folks don't run into the same problems. Also, this example has been tested on Android but it, for the most part, should work on iOS as well. Part of PhoneGap 0.9.6 will be normalizing how the Media class works between all the major platforms so look for improvements soon.

The example will be a basic Media player with play/pause and stop functionality.  Here's a quick screen shot:


On Android you can create a Media object using one of three ways.  The first way is referencing a file in your "android_asset" directory.  For instance if you have a file called "test.mp3" in your projects "assets/www" directory you would create a new media object by doing:
myMedia = new Media("/android_asset/www/test.mp3");
The second way to create a media object is by referencing a file on your devices file system. Say for instance, you have a file at "/sdcard/test.mp3" of your device you would create the media object by doing:
myMedia = new Media("test.mp3");
You'll notice the "/sdcard/" portion of the file name is omitted. This is because on Android the Media object makes an assumption that the file is stored on your "/sdcard". Note: I may change this behaviour in an upcoming version of PhoneGap but I'll strive to retain backward compatibility.

The third and final way to create a media object is by URL:
myMedia = new Media("http://audio.ibeat.org/content/p1rj1s/p1rj1s_-_rockGuitar.mp3");
Now that we've got our media object we want to be able to do the sensible thing and be able to play the audio. For that we'll create a play button:
<a href="#" onclick="playAudio()"><img id="play" src="images/play.png" /></a>
and corresponding function:
function playAudio() {
  myMedia.play();
}
Now the stop button is just as simple:
<a href="#" onclick="stopAudio()"><img id="stop" src="images/stop.png" /></a>
function stopAudio() {
  myMedia.stop();
}
However, that's pretty lame as it would be great to be able to pause the audio playback so let's enhance out play and stop functions:
function playAudio() {
  if (!playing) {
    myMedia.play(); 
    document.getElementById('play').src = "images/pause.png";
    playing = true; 
  } else {
    myMedia.pause();
    document.getElementById('play').src = "images/play.png";    
    playing = false; 
  }
}

function stopAudio() {
  myMedia.stop();
  playing = false;
  document.getElementById('play').src = "images/play.png";    
}
Now when the user is playing some audio the play button image is changed to a pause button and hitting the pause button again will, well, pause the playing of the audio. I'm cheating a little bit by using a global variable called playing as it could easily be changed to check what the currently set image is and doing the correct action.

Of course it is always nice to know where we are at in a given audio clip so we can add some functionality to get the current position of where we are at in the file. So let's create a function that will run every second and update our HTML with the current position of the audio file.
<p id="audio_position">0.000 sec</p>

var mediaTimer = setInterval(function() {
  myMedia.getCurrentPosition(
    function(position) {
      if (position > -1) {
        document.getElementById('audio_position').innerHTML = (position/1000) + " sec";
      }
    },
    function(e) {
      console.log("Error getting pos=" + e);
    }
    );
}, 1000);

That's all well and good but there is one thing that's slipped by our notice so far. When an audio clip finishes playing the pause button should become a play button in case the user wants to play the clip again. That can be accomplished by adding a success call back to our Media object constructor. The success call back is executed when the clip has finished playing. So we'll now instantiate our media like this:
myMedia = new Media("/android_asset/www/test.mp3", stopAudio);
So what we have here are the beginnings of a audio player application for Android. In part two of this article I'll enhance the app with the File System and Directory API showing you how to scan your device for audio files.

The HTML and images can be found here and below is a listing of the HTML code uses in this article. I added a select box so that users could switch between the different ways of creating a Media object. You'll have to supply your own MP3 file as I don't want the RIAA coming after me.

54 comments:

James said...

Hi Simon,

So there seems to be a fourth way to create a Media object and that is with src=null. For iOS, this seems to create something like "documents://1203044830072" where the numbered part is unique with each creation.

What is it creating?

Thanks for your help,
Jo

Simon Mac Donald said...

If I'm not mistaken if you pass src=null to the Media class on iOS it will make it's own file name using the current date. This file name will be used if you call the startAudioRecord() method.

edfilo said...

media.getCurrentPosition and media.getDuration currently do not seem to work in ios , is this support coming in a future release?

Simon Mac Donald said...

Correct, those methods don't work on iOS yet but we are looking into including them for 1.0 due end of July early August.

Rob Archer said...

I've just started developing a media application with PhoneGap, and I noticed that when loading an audio file from a URL, the app becomes unresponsive until it is ready to play. Could this be mitigated by using Web Workers to load them in a different thread?

ibnu Maksum said...

in simulator it works

but in real device its not work

i want to stream shoutcast

Simon Mac Donald said...

@Rob Archer
I think it has to do with the OpenCore instance being allocated by the Android OS.

@Ibnu Maksum
I'll help you over on the google group.

Franco Alvarez said...

Hi Simon, i have one cuestion... I made all what u said.. but allways, no matter what i do and what function use i allways get a prompt with this:

gap:["Media","startPlayingAudio","Media0",true]

????... U have any idea what is this... i found the code in phonegap-1.0.0.0.js this code:

var r = prompt(PhoneGap.stringify(args), "gap:"+PhoneGap.stringify([service, action, callbackId, true]));

and this is the prompt what i getting... Plis help!

PS: sorry for my inglish, i just starting with it

Franco Alvarez said...

thanks for the code!

Franco Alvarez said...

Hi again Simon.. a few seconds I realized that in the BROWSER allways going to get the prompt, in the divice it s Work Amazin! Both codes, your and mine.
Thanks again.

Best regards

Dr Senior said...

In the example above - with the file page "/android_asset/www/test.mp3" ... will this work "out of the box" on an iPhone is the test.mp3 file is in the same place (www/test.mp3)?

Simon Mac Donald said...

@Dr Senior

No, it won't work out of the box on iOS as it has no concept of the android_asset directory. You'll need to do a conditional for now to determine if you are on iOS or Android and pass the appropriate path.

I'm advocating we spend some time in 1.1 to make Media sane across all platforms.

anh said...

Hello, does media.getDuration work yet on iOS?

Simon Mac Donald said...

@anh

It should but if it doesn't open an issue on github and we'll get it fixed.

https://github.com/phonegap/phonegap-iphone/issues

Lars said...

hi, can you explain, how to use a playlist (play several files sequentially)?

Simon Mac Donald said...

@Lars

I've been meaning to write some code to do exactly that. I would keep and array of file names or urls then in my onSuccess method of the Media constructor, which is stopAudio in the example, I would add one to my array index create the new Media object and start playing.

To make things faster I'd probably keep a buffer of 3 to 5 Media objects so that people could skip or go back in the play list.

Arkan said...

Hi Simon,

i wanna add the ID3 in the player,
i used from this library :
http://web.ist.utl.pt/antonio.afonso/www.aadsm.net//libraries/id3/

http://github.com/aadsm/JavaScript-ID3-Reader

this is my code:

var file="/sdcard/music.mp3";
function callback() {
var pic = ID3.getTag(file, "picture");
var artist = ID3.getTag(file, "artist");
var title = ID3.getTag(file, "title");

document.getElementById('judul').innerHTML = artist+ "\r\n";
document.getElementById('judul').innerHTML += title+ "\r\n";
document.getElementById('urlartis').src = "data:" + pic.format + ";base64," + Base64.encodeBytes(pic.data);
}
myMedia.play();
ID3.loadTags(file, callback, ["picture", "artist", "title"]);
playing = true;

it took 40 second to show the artist & title. but the picture not showed at all.

is there a way to speed up this, and also why the image does'nt show.

thanks before for your help.
Ali

Simon Mac Donald said...

@Arkan

Sorry, I can't help you here as I've never used that library but it looks like a cool one. If I had to guess what it is slow is that they are using JavaScript to read a binary file which it doesn't support out of the box. I number of tricks have to be done for the library to read the binary data.

Jithin said...

Hi simon....

Thanks for the tutorial...
I tried it, it working fine in emulator, but it is not working in my device sony erricsson xperia... what is the problem....

Simon Mac Donald said...

@Jithin

The first thing that jumps out at me is that your Sony may not have to proper codec for playing the file. Run "adb logcat" to see what is going on.

Jithin said...

Hi simon, thanks for the quick response.

Can i add volume controller for this app. ?

Regard
Jithin

Simon Mac Donald said...

@Jithin yes, you can. There is a setVolume() method on the Android PhoneGap Media class. It is coming to other platforms.

Jithin said...

Hi simon,

Can u explay how to run "adb log cat", because a m very new to android.

thanks
jithin

Simon Mac Donald said...

@jithin

adb logcat docs.

Jithin said...

thanks simon

Jithin said...

Hi simon,

your demo url http://audio.ibeat.org/content/p1rj1s/p1rj1s_-_rockGuitar.mp3 is working fine in both emulator and device. But i applied another url http://live.ardanradio.com:8228/;audio.mp3. but it is working only in emulator, not working in device. i run adb logcat, it showing D/dalvikvm( 2335): GC freed 7225 objects / 344040 bytes in 100ms
W/MediaPlayer( 2335): info/warning (1, 32)
I/MediaPlayer( 2335): Info (1,32)
D/dalvikvm( 2335): GC freed 20579 objects / 799192 bytes in 118ms
D/dalvikvm( 2335): GC freed 17355 objects / 657824 bytes in 115ms.

and also i checked with this shoutcast url:http://62.75.216.8:8008/;audio.mp3. but same problem

Simon Mac Donald said...

@Jithin

Can't test right now as I am at a conference but try removing the /;audio.mp3 from the end of the URL.

Jithin said...

hi simon

thanks for the quick response,
i tried with http://62.75.216.8:8008 this url. But working only in emulator but not in emulator.

Unknown said...

hello sir me new to phonegap n android development i have audio in binary i.e base 64 format however it does not play on android audio tag.....please help me...

Simon Mac Donald said...

@Unknown the "audio" tag does not work in the WebView for some strange reason. Use the Media class instead.

Simon Mac Donald said...

@Jithin that URL plays fine in the emulator and my Samsung Galaxy S phone. What version of Android is your phone using?

Jithin said...

Hai simon,

i am using samsung galaxy s5570(android 2.3)...

Simon Mac Donald said...

Sorry @Jithin I can't reproduce your problem. I wonder if it is a missing codec problem on your phone as, as you say, it works in the emulator.

Jithin said...

Hi simon,

I don't know what wrong thing i did. i checked with these two url 1. http://62.75.216.8:8008
2. http://stream.lexandterry.com:8000

But first one working only in emulator and second one working in both device(samsung galaxy s) and emulator. After that i checked with sony Ericsson experia. But same problem happening.

Simon Mac Donald said...

Sorry @Jithin, I don't know what to tell you. Some devices may not support streaming audio. I know you need Android 2.2 or better for this to work though.

Unknown said...

hello Simon Sir,

my application requires me to play mp3 BASE64 audio..

phonegap my media doesn't play bas64 audio source...
kindly help Thanks in advance...

Simon Mac Donald said...

@Unknown you'd have to write a plugin to convert your base64 encoded mp3 to a binary file and then pass that the the Media class to be played.

Andrea Levinge said...

Dear Simon,

Thanks for writing this awesome tutorial. I'm having trouble getting the full filepath of the new recorded Media object and uploading it to the server. I wonder if you could suggest how to do it? I've already tried just saving it to android_asset but file upload doesn't work with /android_asset/www/myrecording.mp3 - I know the problem isn't with my file upload methods because I can already upload other files. I just can't get the fullpath of this recording I've made using your recorder/player code.

Simon Mac Donald said...

@Andrea Levinge

That's because the Media class isn't following any know spec. The file name you pass in is assumed to be under /sdcard. So if you pass in temp/data/record.mp3 the file will end up at /sdcard/temp/data/record.mp3.

You cannot save files to the android_assets directory as that is a "virtual" directory that is part of your .apk.

Andrea Levinge said...

Hi Simon,

Thank you so much for replying!
I created a new Media file:

mediaRec = new Media('myrecording.amr', onSuccess, onError);
mediaRec.startRecord();

I want to get that mediaRec's fullPath.

I check where the file is actually located on my sdcard and it's located at /mnt/sdcard/external_sd/

I've tried the following filepaths for uploading:
/mnt/sdcard/external_sd/myrecording.amr
/sdcard/myrecording.amr
myrecording.amr

but the upload always fails as if the file does not exist.

I've successfully uploaded photos and videos already but those were captured using the example code from the API. This recording uses a new Media object. I suspect it is saving it at content://media/external but I don't know how to get the URI of the new Media object I've created. All I know is that photos are getting temporarily saved to content://media/external/images/media/ but I'm sure that's different for audio recordings. I've tried grabbing it from the onSuccess method, but it seems to pass nothing to the onSuccess method when creating a new Media file.

Let us know when your book comes out, I will be ordering it for sure.

kukulako said...

Kukulako from Puerto Rico...

Can I put seek bar or progres bar?

Simon Mac Donald said...

@kukulako

Sure, but you'd have to implement it yourself using getDuration and getCurrentPosition.

John Wargo said...

Simon,

How does this work with PhoneGap Build and PhoneGap 1.2? Where are the media files that are packaged with the application placed and how would I access them cleanly from any platform?

Simon Mac Donald said...

@John

I'm not sure how it would work on the build service as I don't use it. For regular PhoneGap you'd have to do an OS detection and pass the correct prefix along with your file name to the Media constructor.

John Arrowwood said...

This has the potential to be great, but can I have a looping background track with an independent volume, while playing a foreground track, or playlist?

Simon Mac Donald said...

@John A

These are some great ideas and we should make the Media class better for people making games. You should raise a ticket at:

https://issues.apache.org/jira/browse/CB

Uday Kumar said...

Hi Simon,
Please help me out in displaying the All Songs and playlists of android device and how to access android device media table to display the list using phone gap.

Thanks in advance.

Simon Mac Donald said...

Hi Uday, I haven't written the code yet but what you'd need to do is query the content provider for the available music.

http://developer.android.com/guide/topics/providers/content-providers.html

Uday Kumar said...

Thanks Simon for reply,
Is there any way in phonegap to get the data using content provider and display using phonegap javascript and html5. Do i need to write any phone gag plug in to read data using content provider.

Simon Mac Donald said...

@Uday

Yes, you'd need to write a plugin for that type of functionality.

Simon Mac Donald said...

@Andrea

Sorry I missed your comment. After you call stopRecord() the file should be moved to /sdcard. So you should be able to use:

/sdcard/myrecording.amr
of
file:///sdcard/myrecording.amr

Michael J Smith said...

In my app, setVolume() possible values are 0.0 - 1.0. It's looking for a float value.

MrReview said...

Hi Devgeeks,

I'm testing your demo with a shoutcast streaming url and it does'nt work :(

I tested with the streaming url somebody said in this google group.

The debug window in Xcode stucks on this line:
2012-02-08 15:02:21.274 altafullaradio[3219:13403] Will use resource 'http://live.ardanradio.com:8228/;audio.mp3' from the Internet.

I'm using the latest version of phonegap, and iOS 5.0.1

With a normal mp3 url it works good. But with a streaming url it does'nt

What I should try?

Thank you.

Kind regards,

Simon Mac Donald said...

@MrReview

Sounds like a bug in iOS. I'll let Becky or Shaz address this on the google group where you also asked your question.