Monday, November 30, 2015

The ways around J2ME JSR-135 OutOfMemoryException

Playing media in J2ME seems relatively straightforward: provide an InputStream or address and the media should play.  Unfortunately Nokia's implementation of Manager.createPlayer(InputStream, mimeType) will attempt to buffer the stream in full in memory.  It will always run out of memory because it will attempt to buffer the full media object before playing anything back.  A slower stream just makes you wait a bit before it runs out of memory and doesn't play.

Partial Solution: If you use Manager.createPlayer("file://E:/fileonsdcard.3gp") instead with the jad attribute progressive_download: enabled the file will play smoothly and it won't run out of memory (hoorah!).  But what if the media you want to play isn't just sitting around in raw form on the file system?

I tested out using Manager.createPlayer("http://server/some/file.3gp").  Playing over a 2G or 3G internet connection my half hour 3GP clip played OK.  But using a Nokia Asha 500 (which runs series 40) over a WiFi network the clip only plays successfully one time in 5 or so.  Again seems like the underlying logic thinks "AH!!! Data - let's download everything!!!  Ehh... I ate too much... collapse...".  When I used mod_ratelimit in the apache server I was running on my laptop to limit the speed to 128kbps (16K) behavior returned to more or less what it was with the Internet: Generates an interesting Apache log showing that it downloads a small chunk, then a large chunk, then some smaller chunks.  Towards the end playback started sputtering cutting out for 10-15seconds at a time, playing back for less than a minute, and repeating that cycle. - - [30/Nov/2015:12:59:57 +0100] "GET /stream/bbc_click.3gp HTTP/1.1" 200 6847 "-" "Nokia501/2.0 (11.1.1) Profile/MIDP-2.1 Configuration/CLDC-1.1" - - [30/Nov/2015:12:59:57 +0100] "GET /stream/bbc_click.3gp HTTP/1.1" 206 367505 "-" "Nokia501/2.0 (11.1.1) Profile/MIDP-2.1 Configuration/CLDC-1.1" - - [30/Nov/2015:12:59:56 +0100] "GET /stream/bbc_click.3gp HTTP/1.1" 200 130375 "-" "Nokia501/2.0 (11.1.1) Profile/MIDP-2.1 Configuration/CLDC-1.1 UNTRUSTED/1.0" - - [30/Nov/2015:13:00:20 +0100] "GET /stream/bbc_click.3gp HTTP/1.1" 206 16860717 "-" "Nokia501/2.0 (11.1.1) Profile/MIDP-2.1 Configuration/CLDC-1.1" - - [30/Nov/2015:13:23:06 +0100] "GET /stream/bbc_click.3gp HTTP/1.1" 206 37080 "-" "Nokia501/2.0 (11.1.1) Profile/MIDP-2.1 Configuration/CLDC-1.1" - - [30/Nov/2015:13:23:42 +0100] "GET /stream/bbc_click.3gp HTTP/1.1" 206 90564 "-" "Nokia501/2.0 (11.1.1) Profile/MIDP-2.1 Configuration/CLDC-1.1" - - [30/Nov/2015:13:24:24 +0100] "GET /stream/bbc_click.3gp HTTP/1.1" 206 58872 "-" "Nokia501/2.0 (11.1.1) Profile/MIDP-2.1 Configuration/CLDC-1.1" - - [30/Nov/2015:13:25:01 +0100] "GET /stream/bbc_click.3gp HTTP/1.1" 206 77460 "-" "Nokia501/2.0 (11.1.1) Profile/MIDP-2.1 Configuration/CLDC-1.1" - - [30/Nov/2015:13:25:39 +0100] "GET /stream/bbc_click.3gp HTTP/1.1" 206 30528 "-" "Nokia501/2.0 (11.1.1) Profile/MIDP-2.1 Configuration/CLDC-1.1" - - [30/Nov/2015:13:26:15 +0100] "GET /stream/bbc_click.3gp HTTP/1.1" 206 147324 "-" "Nokia501/2.0 (11.1.1) Profile/MIDP-2.1 Configuration/CLDC-1.1" - - [30/Nov/2015:13:26:57 +0100] "GET /stream/bbc_click.3gp HTTP/1.1" 206 629964 "-" "Nokia501/2.0 (11.1.1) Profile/MIDP-2.1 Configuration/CLDC-1.1" - - [30/Nov/2015:13:28:09 +0100] "GET /stream/bbc_click.3gp HTTP/1.1" 206 81803 "-" "Nokia501/2.0 (11.1.1) Profile/MIDP-2.1 Configuration/CLDC-1.1" - - [30/Nov/2015:13:28:48 +0100] "GET /stream/bbc_click.3gp HTTP/1.1" 206 30527 "-" "Nokia501/2.0 (11.1.1) Profile/MIDP-2.1 Configuration/CLDC-1.1" - - [30/Nov/2015:13:29:23 +0100] "GET /stream/bbc_click.3gp HTTP/1.1" 206 159739 "-" "Nokia501/2.0 (11.1.1) Profile/MIDP-2.1 Configuration/CLDC-1.1"

Now let's consider some workarounds one might have in mind: and how unfortunately they get thwarted:

  • If I know the duration of a stream: slow the stream down to the rate of playback.  
    • Unfortunately the duration is not available for players created using an InputStream : and the duration is also not available ; so calculating the bitrate would involve manual file reading and codec work.
  • Use a DataSource / SourceStream for Manager.createPlayer
    • I didn't have any luck with this one: I tested my implementation on the emulator with a wav file - was happy.  Tested it on a Series 40 phone: can't get beyond "Error connecting to data source"
  • Let's close the stream at some time to force an interruption / replay
    • Getting this to play in the best of circumstances isn't so much fun... and partial streams (e.g. truncated files) aren't understood
  • Let's slow down the stream to internet like speeds: then use an Internal HTTP Server
    • Well this should produce the same performance as running it over the internet: the problem is the implementation of playing over HTTP misbehaves.
Which leaves us with one final workaround: Write the entire stream to a file and then use Manager.createPlayer with the created file URL.  In our particular implementation we have files downloaded locally but in epub (zipped) files.  Unzipping an 18MB 3gp file (which plays for 30mins) took 2mins on a Nokia 206 (mid range phone).  

For a good overview (that even mentions the let's buffer everything problem) check out this post on InformIT .

Wednesday, November 25, 2015

Using InputStreams with JSR-135 J2ME Sound Playback

In our J2ME app we need to be able to play back sounds for the user for files that are already downloaded (albeit inside zipped epubs).  So connecting the two using Manager.createPlayer(in, mimeType) seems fairly straightforward.  However on the devices themselves with longer files it will croak after a certain amount of time (maybe a buffer under run happens... still not sure).  With some other files that we were accessing for performance and to ensure any active file connections are closed properly we simply read the whole content into a byte array.  Not a good idea when it comes to long mp3 files: here's our recipe that seems to work:

  1. Obtain an inputstream from the file source (in our case, the file source is inside a zip for which we use the gnu classpath project to unzip)
  2. Also from gnu classpath get the BufferedInputStream class (this is not supplied in on J2ME/CLDC devices).
  3. Feed the player a BufferedInputStream instead of the raw stream itself: e.g.
    return new BufferedInputStream(src, 20*1024);
I haven't yet played around with the buffer sizes; occasionally it very briefly misses a fraction of a beat on lower end devices (e.g. the Nokia 110)

Monday, November 9, 2015

J2ME Images stop loading mystery

In our app we need to be able to show images from EPUB files on J2ME .. and those images change as the user goes from page to page.  We're using the rather neat LWUIT HTMLComponent to render the HTML pages with some custom additions to handle media (published here on GitHub).

Mysteriously using a Nokia 206 phone we could load about 19 900x900 pixel jpegs (showing one after the other; with a garbage collect System.gc() call in between) before it would give up - and any subsequent request to load an image would throw an IOException.  This was being caused by an IOException being thrown inside the ResourceThread as it was trying to load the image.  This of course was happening only on the devices: not on the emulator.

J2ME phones is that they vary widely in resolution and memory - from a Nokia 110 which has a 120 pixel or so wide screen to an Asha which has 240x320.  Perhaps images can simply be bounded to a maximum of 320x320 ; or perhaps we need to have different epub files for different phones.  Not sure of that yet.

Note to anyone (still) working with J2ME to avoid tearing hair out : keep images as small as they can be.