Cross-platform RIFF AVI player (pure .NET)

The AVI player in this Lode Runner Online: The Mad Monks' Revenge rewrite project was finished back in March this year. You can read little bits of it here and here. I noticed that I didn't have a complete post on this topic so here you go.

Why the need to play RIFF AVIs?

The cut-scenes in Lode Runner: The Legend Returns use a propriety Presage format that given countless hours, I've managed to figure out and implement. I left doing these to later as from early on I realized that these video files relied on several other propriety Presage formats that I had to crack first. There were a few bumps along the way but overall, it was rather straight-forward.

Unfortunately, when Presage made Lode Runner Online: The Mad Monks' Revenge, they used basic RIFF AVI video files for the two new worlds (Astro and Aqua) and the new ending cut-scene. In order to stick with my goal of trying to keep as many of the original resources as possible, I'd need an AVI player that had to meet these criteria;

  • must be cross-platform - Windows, Linux and OS X
  • the same code for all platforms
  • no external dependencies

With the above in mind and lots and lots and lots of research, I came up with a list of challenges that needed to be overcome;

  • Microsoft DirectX is Windows only - DirectX is by far the easiest way to play videos but not cross-platform
  • using Windows Media Player components is also easy, but again, not cross-platform
  • no 3rd-party media players available that don't rely on DirectX
  • no AVI parsers/players written in C# or VB.NET that are not too complex or rely on other libraries

If you are in the same boat as I was, but don't care about keeping old video formats, remember that FNA does have support to play Vorbis videos. If you're using MonoGame, you might be able to load AVIs through the content pipeline but I could be wrong on that.

Beginning

To start with, I had to write a basic parser for the 1992 era Microsoft RIFF AVI specs. Luckily, the complete reference material is available for free on Microsoft MSDN. A lot of the information is a little mind boggling to understand. I went for the bare minimum that the Lode Runner AVI files needed. All the data structures were just copied out of MSDN and tweaked for VB. That was that, rather straight-forward.

I tested the parser on the two world cut-scenes and it worked but failed on the ending cut-scene. After some tweaks, the parser worked for all three of the video files. Without boasting too much, my parser is rather compact and well written. The only downside being no-frills (75 lines of code) - it only really works with the Lode Runner videos. That is okay with me since that is all I will be using it for.

Creating the player

Playing a RIFF AVI is rather straight-forward;

  • parse the video file
  • play;
  • extract a record
  • convert the image in the record to a texture and draw it
  • play the PCM audio from the record
  • rinse and repeat

I got a little confused reading the AVI specs which meant I ended up not drawing the video correctly as you can see below. I was not drawing the current frame over the top of the previous frame.

Extracting frames

The Lode Runner Online videos use the basic Microsoft RLE8 compression for each frame. Run-length encoding is a basic compression algorithm that (from Wikipedia):

Run-length encoding (RLE) is a very simple form of data compression in which runs of data (that is, sequences in which the same data value occurs in many consecutive data elements) are stored as a single data value and count, rather than as the original run.

Below is a VB method to decompress a frame. I've modified it to make it simpler but you need to take note of a few things mentioned below. The code was ported from Dick Bertels' site from C. If you are interested in an overview of how RLE bitmap compression works, MSDN has a nice article.

To make the function work you need;

  • to have the video stream loaded (_stream = binary reader) and at the start of a frame to decompress
  • to know the width and height of the frame and have
  • the colour palette loaded
    ''' 
    ''' Decompress image data encoded with Microsoft RLE8 codec.
    ''' 
    ''' The decoded frame as an array of colours
    ''' http://dirkbertels.net/computing/bitmap_decompression.php
    Private Function DecompressRLE8() As Color()

        Dim firstByte As Byte
        Dim secondByte As Byte
        Dim currX As Integer = 0
        Dim currY As Integer = _bitmap.biHeight - 1
        Dim i As UInteger
        Dim image As Color(_bitmap.biWidth* _bitmap.biHeight - 1) {}

        Do
            firstByte = _stream.ReadByte
            If 0 <> firstByte Then
                ' Encoded Mode
                secondByte = _stream.ReadByte
                For i = 0 To firstByte - 1
                    image(currY * frameSize.Width + currX) = _bitmap.biClut(secondByte)
                    currX += 1
                Next i
            Else
                ' Absolute Mode
                firstByte = _stream.ReadByte ' store next byte
                Select Case firstByte
                    Case 0 ' new line
                        currX = 0
                        If currY > 0 Then
                            currY -= 1
                        End If
                    ' move cursor to beginning of next line
                    Case 1 ' end of bitmap
                        Exit Sub
                    Case 2 ' delta = goto X and Y
                        currX += Convert.ToInt32(_stream.ReadByte) ' read byte and add value to x value
                        currY -= Convert.ToInt32(_stream.ReadByte) ' read byte and add value to y value
                    Case Else
                        For i = 0 To firstByte - 1
                            secondByte = _stream.ReadByte
                            image(currY * frameSize.Width + currX) = _bitmap.biClut(secondByte)
                            currX += 1
                        Next i
                        If firstByte And &H1 Then ' if the run doesn't end on a word boundary,
                            _stream.ReadByte()
                        End If
                End Select
            End If
        Loop

        Return image
    End Sub

Extracting audio

You're supposed to stream and play the audio per record but for me I get a little crackle every few frames. Buffering a little more audio doesn't really help and usually causes the audio to get out of sync. I'm probably overlooking something simple but after months of on and off tinkering, I could get never get it to work.

The workaround I came up with for this project was rather simple: extract the entire audio stream in one go and play it. Totally not ideal for large videos but for small videos <2mb like MMR, you should be able to sneaky do it like me.

    ''' 
    ''' Converts PCM audio data to a 16-bit sound effect.
    ''' 
    ''' The sample rate, in samples per second (hertz), of the audio.
    ''' The number of audio channels the PCM samples have (mono or stereo).
    ''' Raw audio data + Presage header.
    ''' A sound effect.
    Public Shared Function ConvertToEffect(sampleRate As Integer, channels As AudioChannels, data As Byte()) As SoundEffect

        ' Convert the PCM from 8-bit to 16-bit.
        Dim buffer((data.Length * 2) - 1) As Byte
        Dim sample16 As Int16
        Dim sample16_bytes() As Byte
        For i As Integer = 8 To data.Count - 1 ' data holds 8bit data
            sample16 = (data(i) - 128) << 8
            sample16_bytes = BitConverter.GetBytes(sample16)
            buffer(i * 2) = sample16_bytes(0)
            buffer(i * 2 + 1) = sample16_bytes(1)
        Next

        Return New SoundEffect(buffer, sampleRate, channels)
    End Function

And that's a wrap

So, there you are. I know I haven't shared much code or claimed to have made anything ground-breaking, but I hope that you found it even a smidge interesting or even useful.

  • 100% Visual Basic.NET code
  • compact code
  • zero reliance on DirectX
  • no 3rd-party libraries used
  • cross-platform; works on Windows, Linux and OS X

Most importantly, it allows me to keep the original resources and not have to re-encode them or write three lots of code for three different platforms.

And now for a demonstration of the final product straight-off my YouTube channel (that you've probably already watched).

*Aqua world cut-scene*