There are a number of ways to get pixels on the screen in Silverlight 3.
One you may not have considered, especially with the new Bitmap API
being an otherwise obvious choice, is the newly enhanced
MediaStreamSource API
The MediaStreamSource API was available in Silverlight 2,
but it required you to transcode into a supported format. The new
version of the API lets you create raw video images much more
easily. There are a number of use-cases for this including complete
bitmap-based games, creating chroma-keyed/alpha video, and in my
case, using it to show the display of my Commodore 64 emulator at
50 frames per second (PAL standard)
(You can also use the MediaStreamSource to create audio from raw
bits. Look for that in another upcoming post.)
So how hard is this to do? Well, once you get the basic setup
down, it's actually really easy. The thing to remember is your code
will be called by Silverlight, not the other way around. Therefore
you just need to respond appropriately when, for example,
Silverlight requests the next frame.
First, you want to create a class that inherits from
MediaStreamSource
public class VideoMediaStreamSource : MediaStreamSource
Pixel Buffers
The emulated MOSS 6569 VIC-II chip (the equivalent of a video card
today) in my code pushes pixels out to my media stream source until
it reaches the end of the current frame. Once that happens, it
calls Flip(). I keep two pixel buffers around to eliminate any
tearing or half-updates on the screen where Silverlight requests a
frame that is not yet complete. One buffer is what the VIC is
writing to, the other is what Silverlight is reading from. When the
VIC calls Flip(), I swap the two.
private byte[][] _frames = new byte[2][];
public VideoMediaStreamSource(int frameWidth, int frameHeight)
{
_frameWidth = frameWidth;
_frameHeight = frameHeight;
_framePixelSize = frameWidth * frameHeight;
_frameBufferSize = _framePixelSize * BytesPerPixel;
// PAL is 50 frames per second
_frameTime = (int)TimeSpan.FromSeconds((double)1 / 50).Ticks;
_frames[0] = new byte[_frameBufferSize];
_frames[1] = new byte[_frameBufferSize];
_currentBufferFrame = 0;
_currentReadyFrame = 1;
}
(yes, I could have done something clever with xor here)
public void Flip()
{
int f = _currentBufferFrame;
_currentBufferFrame = _currentReadyFrame;
_currentReadyFrame = f;
}
Writing a pixel is pretty easy, as long as you get the format
correct. Notice the order of the component R, G, B and A parts.
BytesPerPixel is defined as 4 as this is a 32 bit color value.
public void WritePixel(int position, Color color)
{
int offset = position * BytesPerPixel;
_frames[_currentBufferFrame][offset++] = color.B;
_frames[_currentBufferFrame][offset++] = color.G;
_frames[_currentBufferFrame][offset++] = color.R;
_frames[_currentBufferFrame][offset++] = color.A;
}
Opening your Video
Surprisingly, this is much easier for video than it is for
audio. For audio, you need to fill out a whole WaveFormatEx
structure. For video, you simply need to set the appropriate FourCC
code (like RGBA or YV12), the frame width and the frame height.
protected override void OpenMediaAsync()
{
// Init
Dictionary<MediaSourceAttributesKeys, string> sourceAttributes =
new Dictionary<MediaSourceAttributesKeys, string>();
List<MediaStreamDescription> availableStreams =
new List<MediaStreamDescription>();
PrepareVideo();
availableStreams.Add(_videoDesc);
// a zero timespan is an infinite video
sourceAttributes[MediaSourceAttributesKeys.Duration] =
TimeSpan.FromSeconds(0).Ticks.ToString(CultureInfo.InvariantCulture);
sourceAttributes[MediaSourceAttributesKeys.CanSeek] = false.ToString();
// tell Silverlight that we've prepared and opened our video
ReportOpenMediaCompleted(sourceAttributes, availableStreams);
}
private void PrepareVideo()
{
_frameStream = new MemoryStream();
// Stream Description
Dictionary<MediaStreamAttributeKeys, string> streamAttributes =
new Dictionary<MediaStreamAttributeKeys, string>();
streamAttributes[MediaStreamAttributeKeys.VideoFourCC] = "RGBA";
streamAttributes[MediaStreamAttributeKeys.Height] = _frameHeight.ToString();
streamAttributes[MediaStreamAttributeKeys.Width] = _frameWidth.ToString();
MediaStreamDescription msd =
new MediaStreamDescription(MediaStreamType.Video, streamAttributes);
_videoDesc = msd;
}
Requesting Frames
Ignore for a moment that the code checks for both audio and
video here. While I have snipped some of that from the other
examples above, the code is actually set up to eventually serve up
the audio stream from the Silverlight SID chip as well as the video from the VIC
chip.
protected override void GetSampleAsync(MediaStreamType mediaStreamType)
{
if (mediaStreamType == MediaStreamType.Audio)
{
GetAudioSample();
}
else if (mediaStreamType == MediaStreamType.Video)
{
GetVideoSample();
}
}
Now in the GetVideoSample() method, I'm creating a new stream
with each request. I don't like the approach, but I was getting out
of memory errors with early Silverlight 3 builds if I tried to
reuse the stream and simply provide offsets into it. I'll debug
this some more before posting the final code.
private void GetVideoSample()
{
_frameStream = new MemoryStream();
_frameStream.Write(_frames[_currentReadyFrame], 0, _frameBufferSize);
// Send out the next sample
MediaStreamSample msSamp = new MediaStreamSample(
_videoDesc,
_frameStream,
0,
_frameBufferSize,
_currentVideoTimeStamp,
_emptySampleDict);
_currentVideoTimeStamp += _frameTime;
ReportGetSampleCompleted(msSamp);
}
One key part of the code above is the _currentVideoTimeStamp +=
_frameTime. Frame Time is a constant equal to 1/50 of a second in
100 nanosecond intervals. Setting the frametime there tells
Silverlight that we're serving up 50 frames per second. I haven't
tried varying that number, but you could probably get some
interesting performance characteristics by playing around with that
in real-time.
Wiring it Up
Hooking it up to Silverlight couldn't be easier:
<MediaElement x:Name="VideoDisplay"
Grid.Row="0"
Grid.Column="0"
Stretch="Uniform"
IsHitTestVisible="False"
Margin="4" />
VideoDisplay.SetSource(_c64.Display.Video);
_c64.Display.Video is an instance of my MediaStreamSource
class.
Conclusion
So there you have it, runtime-generated video inside a
Silverlight 3 application. Pretty cool if you ask
me!
I'll make all of this code available as part of various projects
on codeplex later. Stay tuned. In the mean time, the complete source code for this one class is
available here.