Bluish Coder

Programming Languages, Martials Arts and Computers. The Weblog of Chris Double.


2009-06-24

Reading Ogg files using libogg

Reading data from an Ogg file is relatively simple. The file format is well documented in RFC 3533. I showed how to read the format using JavaScript in a previous post.

For C and C++ programs it's easier to use the xiph.org libraries. There are libraries for decoding specific formats (libvorbis, libtheora) and there is a library for reading data from Ogg files (libogg).

I'm prototyping some approaches to improve the performance of the Firefox Ogg video playback and while I'm at it I'll write some posts on using these libraries to decode/play Ogg files. Hopefully it'll prove useful to others using them and I can get some feedback on usage.

All the code for this is in the plogg git repository on github. The 'master' branch contains the work in progress player that I'll describe in a series of posts, and there are branches specific to the examples in each post.

The libogg documentation describes the API that I'll be using in this post. All that this example will do is read an Ogg file, read each stream in the file and count the number of packets for that stream. It prints the number of packets. It doesn't decode the data or do anything really useful. That'll come later.

You can think of an Ogg file as containing logical streams of data. Each stream has a serial number that is unique within the file to identify it. A file containing Vorbis and Theora data will have two streams. A Vorbis stream and a Theora stream.

Each stream is split up into packets. The packets contain the raw data for the stream. The process of decoding a stream involves getting a packet from it, decoding that data, doing something with it, and repeating.

That describes the logical format. The physical format of the Ogg file is split into pages of data. Each physical page contains some part of the data for one stream.

The process of reading and decoding an Ogg file is to read pages from the file, associating them with the streams they belong to. At some point we then go through the pages held in the stream and obtain the packets from it. This is the process the code in this example follows.

The first thing we need to do when reading an Ogg file is find the first page of data. We use a ogg_sync_state structure to keep track of search for the page data. This needs to be initialized with ogg_sync_init and later cleaned up with ogg_sync_clear:

ifstream file("foo.ogg", ios::in | ios::binary);
ogg_sync_state state;
int ret = ogg_sync_init(&state);
assert(ret==0);
...look for page...

Note that the libogg functions return an error code which should be checked, A result of '0' generally indicates success. We want to obtain a complete page of Ogg data. This is held in an ogg_page structure. The process of obtaining this structure is to do the following steps:

  1. Call ogg_sync_pageout. This will take any data current stored in the ogg_sync_state object and store it in the ogg_page. It will return a result indicating when the entire pages data has been read and the ogg_page can be used. It needs to be called first to initialize buffers. It gets called repeatedly as we read data from the file.
  2. Call ogg_sync_buffer to obtain an area of memory we can reading data from the file into. We pass the size of the buffer. This buffer is reused on each call and will be resized if needed if a larger buffer size is asked for later.
  3. Read data from the file into the buffer obtained above.
  4. Call ogg_sync_wrote to tell libogg how much data we copied into the buffer.
  5. Resume from the first step, calling ogg_sync_buffer. This will copy the data from the buffer into the page, and return '1' if a full page of data is available.

Here's the code to do this:

ogg_page page;
while(ogg_sync_pageout(&state, &page) != 1) {
  char* buffer = ogg_sync_buffer(oy, 4096);
  assert(buffer);

  file.read(buffer, 4096);
  int bytes = stream.gcount();
  if (bytes == 0) {
    // End of file
    break;
  }

  int ret = ogg_sync_wrote(&state, bytes);
  assert(ret == 0);
}

We need to keep track of the logical streams within the file. These are identified by serial number and this number is obtained from the page. I create a C++ map to associate the serial number with an OggStream object which holds information I want associated with the stream. In later examples this will hold data needed for the Theora and Vorbis decoding process.

class OggStream
{
  ...
  int mSerial;
  ogg_stream_state mState;
  int mPacketCount;
  ...
};

typedef map<int, OggStream*> StreamMap; 

Each stream has an ogg_stream_state object that is used to keep track of the data read that belongs to the stream. We're storing this in the OggStream object that we associated with the stream serial number. Once we've read a page as described above we need to tell libogg to add this page of data to the stream.

StreamMap streams;
ogg_page page = ...obtained previously...;
int serial = ogg_page_serialno(&page);
OggStream* stream = 0;
if (ogg_page_bos(&page) {
  stream = new OggStream(serial);
  int ret = ogg_stream_init(&stream->mState, serial);
  assert(ret == 0);
  streams[serial] = stream;
}
else
  stream = streams[serial];

int ret = ogg_stream_pagein(&stream->mState, &page);
assert(ret == 0);

This code uses ogg_page_serialno to get the serial number of the page we just read. If it is the beginning of the stream(ogg_page_bos) then we create a new OggStream object, initialize the stream's state with ogg_stream_init, and store it in out streams map. If it's not the beginning of the stream we just get our existing entry in the map. The final call to ogg_stream_pagein inserts the page of data into the streams state object. Once this is done we can start looking for completed packets of data and decode them.

To decode the data from a stream we need to retrieve a packet from it. The steps for doing this are:

  1. Call ogg_stream_packetout. This will return a value indicating if a packet of data is available in the stream. If it is not then we need to read another page (following the same steps previously) and add it to the stream, calling ogg_stream_packetout again until it tells us a packet is available. The packet's data is stored in an ogg_packet object.
  2. Do something with the packet data. This usually involves calling libvorbis or libtheora routines to decode the data. In this example we're just counting the packets.
  3. Repeat until all packets in all streams are consumed.

The code for this is:

while (..read a page...) {
  ...put page in stream...  
  ogg_packet packet;
  int ret = ogg_stream_packetout(&stream->mState, &packet);    
  if (ret == 0) {
    // Need more data to be able to complete the packet
    continue;
  }
  else if (ret == -1) {
    // We are out of sync and there is a gap in the data.
    // We lost a page somewhere.
    break;
  }

  // A packet is available, this is what we pass to the vorbis or
  // theora libraries to decode.
  stream->mPacketCount++;
}

That's all there is to reading an Ogg file. There are more libogg functions to get data out of the stream, identify end of stream, and various other useful functions but this covers the basics. Try out the example program in the github repository for more information.

Note that the libogg functions don't require reading from a file. You can use these routines with any data you've obtained. From a socket, from memory, etc.

In the next post about reading Ogg files I'll go through using libtheora to decode the video data and display it.

Tags: ogg  mozilla 

2009-06-05

Reading Ogg files with JavaScript

On tinyvid.tv I do quite a bit of server side reading of Ogg files to get things like duration and bitrate information when serving information about the media. I wondered if it would be possible to do this sort of thing using JavaScript running in the browser.

The format of the Ogg container is defined in RFC 3533. The difficulty comes in reading binary data from JavaScript. The XMLHttpRequest object can be used to retrieve data via a URL from JavaScript in a page but processing the binary data in the Ogg file is problematic. The response returned by XMLHttpRequest assumes text or XML (in Firefox at least).

One way of handling binary data is described in this Mozilla Developer article. Trying this method out works in Firefox and I can download and read the data in the Ogg file.

Ideally I don't want to download the entire file. It might be a large video. I thought by handling the 'progress' event or ready state 3 (data received) I'd be able to look at the data currently retrieved. This does work but on each call to the 'responseText' attribute in these events Firefox copies its internal copy of the downloaded data into a JavaScript array. Doing this every time a portion of the file is downloaded results in major memory use and slow downs proving impractical for even small files.

I think the only reliable way to process the file in chunks is to use byte range requests and do multiple requests. Is there a more reliable way to do binary file reading via JavaScript using XMLHttpRequest? I'd like to be able to process the file in chunks using an Iteratee style approach.

I put up a rough quick demo of loading the first 100Kb of a video and displaying information from each Ogg packet. This probably works in Firefox only due to the workaround needed to read binary data. Click on the 'Go' button in the demo page. This will load transformers320.ogg and display the contents of the first Ogg physical page.

I decode the header packets for Theora and Vorbis. So the first page shown will show it is for a Theora stream with a given size and framerate. Clicking 'Next' will move on to the Next page. This is a Vorbis header with the rate and channel information. Clicking 'Next' again gets the comment header for the Theora stream. The demo reads the comments and displays them. The same for the Vorbis comment records. As you 'Next' through the file it displays the meaning of the granulepos for each page. It shows whether the Theora data is for a keyframe, what time position it is, etc.

Something like this could be used to read metadata from Ogg files, read subtitle information, show duration, etc. More interesting would be to implement a Theora and/or Vorbis decoder in JavaScript and see how it performs.

The main issues with doing this from JavaScript seem to be:

  • Handling binary data using XMLHttpRequest in a cross browser manner
  • Processing the file in chunks so the entire file does not need to be kept in memory
  • Files need to be hosted on the same domain as the page. tinyvid.tv adds the W3C Access Control headers so they can be accessed cross domain but it also hosts some files on Amazon S3 where these headers can't be added. As a result even tinyvid itself can't use XMLHttpRequest to read these files.
Tags: tinyvid  javascript  ogg  mozilla 

2009-06-02

Video for Everybody - HTML 5 video fallback

Kroc Camen has made available Video for Everybody, an HTML snippet that uses HTML 5 video if it's available in the browser, otherwise falling back to different video playback options.

What's interesting about 'Video for Everybody' is it doesn't use scripting at all. It uses the fallback mechanism built into HTML. The video playback mechanism used, in order of availability in the browser, is:

  1. HTML 5 video
  2. Adobe Flash
  3. Quicktime
  4. Windows Media Player
  5. Text explaining how to get video support if none of the above is available

The fallback options work across multiple browsers and even works on the iPhone. The 'Video For Everybody' page also goes through how to encode videos in Ogg and MP4 format.

Tags: video  mozilla 

2009-05-16

Third Party Comment Engines

I've been playing around with different commenting engines on tinyvid.

Currently I use Intense Debate. An example of a video using this comment system is here. I like this service a lot. You can sign on with OpenId, the moderation tools are good and it seems to be generally reliable.

Intense Debate has support for plugins that anyone can write. Potentially I could write a video plugin that allowed embedding <video> into comments for example.

But it does have some issues. Sometimes comments don't seem to appear in the right order. I'll get an email that a comment was left, visit the page, and it's nowhere. Somehow it gets buried amongst other older comments. This started happening when I switched the ordering of the comments to have the most recent comment at the top.

Sometimes, depending on the content of your comment, the engine gets confused and the comment is scrambled. This seems to happen if I edit a comment that has a URL in it. It converts the URL to an HTML <a href/> and when editing and saving seems to reconvert, or do some weirdness with it. I've lost comments through this. Long comments can get silently truncated or trashed.

I set up tinyvid so I can easily add different comment engines and switch them on and off for individual videos for testing. I recently tried js-kit. An example of video with js-kit comments for testing is here.

I like js-kit too. I'm using their 'rating' widget and that seems to work nicely. The comments are ok too. You can sign in with OpenId. But there is some weirdness there. If I sign in using OpenId, leave a comment, then revisit the page later, then the comments don't appear. I have to delete the js-kit cookie to have them appear again.

There is a limit to the size of the comments, similar in size to Intense Debate's, of 3,000 characters. I suspect this limit isn't a problem in practice however.

There's a lot to like about js-kit, apart from the OpenId issue, but I need to try it out a bit more to see what other issues it has.

The last one I tried was Disqus. Example here. The Disqus site lets you register using OpenId. However you can't login to leave a comment using OpenId. You must login using a Disqus Id that gets assigned to you when you've registered.

When I registered using OpenId, then tried to login to leave a comment it requested a password. I had to go back to the Disqus site, change my profile to add a password. This seems sub-optimal since the point of me using OpenId is to avoid multiple passwords and accounts.

Disqus requires anonymous posters to leave a valid email address. I couldn't find a way to turn this option off. I'm sure this will just result in people leaving bogus email addresses.

One step of the Disqus setup requires adding a script block to the page that does document.write. This won't work on XHTML sites. And I dislike document.write usage in general. Thankfully this step appears optional. It's for converting marked up items in the page to a comment count.

After leaving a comment Disqus does a full page refresh. This unfortunately results in the video on the page restarting and reloading.

Apart from those issues Disqus seemed quite nice. Simple to set up and configurable.

So the end result is I'm not sure which is best. I'll play around with them for a bit more and get feedback. Are there any other's worth trying?

Tags: tinyvid 

2009-05-08

Theora encoder improvements

A lot of work has been going on improving the Theora video encoder. Given a better encoder we end up with better quality Theora videos without the need to change the decoder in existing players. The project name for the Theora encoder improvements is 'Thusnelda'.

Periodic updates have been released by Xiph showing the progress of the new encoder. The latest update makes interesting reading and show's graphs comparing it against other Theora builds, and against x264 (an H.264 encoder).

The update show's that some great improvements have been made, and in some cases is comparible to the results from the x264 encoder. Greg Maxwell gives some more information in this reddit comment about the results.

Remember that the intent of this is not to compare Theora vs x264, or to say one is better than the other in the general case. It's to show the improvement of Theora vs older Theora versions. The fact that it compares favourably against x264 in this one case, using one particular test of quality, is a good result though.

Tags: video  mozilla 


This site is accessable over tor as hidden service 6vp5u25g4izec5c37wv52skvecikld6kysvsivnl6sdg6q7wy25lixad.onion, or Freenet using key:
USK@1ORdIvjL2H1bZblJcP8hu2LjjKtVB-rVzp8mLty~5N4,8hL85otZBbq0geDsSKkBK4sKESL2SrNVecFZz9NxGVQ,AQACAAE/bluishcoder/-61/


Tags

Archives
Links