Video Playback
Implementation status: VideoPlayer is real where the optional FFmpeg backend is built — it genuinely decodes video through FFmpeg (real libavcodec, libavformat and libswresample, with PTS-driven pacing), not a placeholder that returns blank frames. Since this snapshot the FFmpeg backend is optional: the Video and VideoPlayer types exist and link in every build, and a build without the backend reports System::NotSupportedException at run time instead of failing to link.
Decoding is available only where the FFmpeg backend was built — today native Linux and, per CNA's notes, macOS. The backend is selected by the CMake option CNA_ENABLE_VIDEO (AUTO by default): on Linux and macOS it is used when the libavcodec, libavformat, libavutil and libswresample development packages are found, and otherwise the build falls back. On Windows (MSVC and MinGW), Emscripten, Android and iOS the backend is never built (-DCNA_ENABLE_VIDEO=ON there is a configure error). Nothing fails to link, though: without the backend, creating a file-backed Video for an existing file, or calling VideoPlayer::Play with one, throws System::NotSupportedException (a missing file still throws FileNotFoundException), and FFmpeg is no longer a hard requirement for the project. Metadata-only Video objects and the XNB video reader work in every build. Guard video with CNA_VIDEO_AVAILABLE (defined only when the backend is present) or catch the exception, as the examples below do.
Overview
VideoPlayer and Video live in the Microsoft::Xna::Framework::Media namespace, matching the XNA 4.0 API. CNA uses FFmpeg as its optional video decoding back-end (a separate CNA::VideoFfmpeg module): the decoder reads compressed frames from the container, converts them from YUV colour space to RGBA, and uploads the result as a Texture2D. Your Draw() method retrieves that texture each frame via VideoPlayer::GetTexture() and renders it with SpriteBatch (or any other drawing path) exactly like any other Texture2D.
This is a real decoder, and it is no longer the only implemented part of Media. MediaPlayer genuinely plays songs, while MediaLibrary resolves the user's music and pictures directories, scans them recursively, parses common audio tags and playlists, discovers album art, and persists pictures. Treat individual device/library behaviors as platform-dependent rather than as a Windows Phone or Zune service emulation.
Frame timing is driven by the stream's own presentation timestamps rather than by a fixed frame counter, so playback tracks the container's real timeline instead of drifting against it.
Verification is configuration-scoped. This snapshot has 30 Media test sources with 304 statically discoverable GoogleTest-family definitions (alpha.1: 29 and 286). CNA records that both the CNA_ENABLE_VIDEO=ON and OFF configurations are built and their video contracts tested on Linux, but the decoder itself exists only where FFmpeg was found, and the optional split has not been run on macOS by its author. Run the target configuration you intend to ship.
Supported container and codec combinations:
- MP4 — H.264 / H.265
- OGV — Ogg Theora
- WEBM — VP8 / VP9
- MKV — Matroska (any FFmpeg-supported codec)
- AVI — legacy container
- MOV — QuickTime container
Platform restriction — the FFmpeg backend is NOT built on:
- Emscripten / WebAssembly — use the HTML5
<video>element for in-browser video playback instead. - Android — use the Android
MediaPlayerAPI directly. - Every Windows target — both the MinGW-w64 cross-toolchain and native MSVC. Windows builds have no video decoding.
- iOS — the host macOS FFmpeg is deliberately not used for an iOS cross-build.
That leaves Linux and macOS desktop builds as the only places where VideoPlayer can decode anything. On every other target it throws NotSupportedException when you try to play a file.
Video Class
A Video object represents a video asset loaded from disk. Load it through the standard ContentManager pipeline:
// LoadContent() — Load<T> returns the Video by value
Video video = getContentProperty().Load<Video>("videos/intro");
The ContentManager resolves the file extension automatically (.mp4, .ogv, .webm, .mkv, .avi, .mov as loose files); the path should be relative to the content root and omit the extension. A compiled .xnb or .cnb video asset loads too: the built-in VideoReader is registered in every build and carries the metadata, and the media file itself is referenced, not embedded. Video has no default constructor (only CNAEXT constructors taking a file name and a graphics device, with optional metadata), so keep it in a std::optional<Video> or a std::unique_ptr when it is a class member. Once loaded, the following read-only properties are available:
| Property | Type | Description |
|---|---|---|
getWidthProperty() |
int |
Frame width in pixels |
getHeightProperty() |
int |
Frame height in pixels |
getDurationProperty() |
System::TimeSpan |
Total playback duration of the video |
getFramesPerSecondProperty() |
float |
Native frame rate reported by the container |
getVideoSoundtrackTypeProperty() |
VideoSoundtrackType |
Indicates whether the video has a music track, dialog, or no audio (Music, Dialog, MusicAndDialog) |
VideoPlayer Class
VideoPlayer controls playback of a Video asset. Create one instance per active video stream; it is not designed for concurrent playback of multiple videos from the same instance.
Methods
| Method | Description |
|---|---|
Play(Video*) |
Starts playback of the given video from the beginning. If a video is already playing it is replaced. Without the FFmpeg backend this throws System::NotSupportedException before changing the player's video or state. |
Pause() |
Pauses playback at the current frame. The decoded texture remains valid. |
Resume() |
Resumes playback from the paused position. |
Stop() |
Stops playback and rewinds to the beginning. |
GetTexture() |
Returns a Texture2D* containing the current decoded frame. Call this every frame inside Draw(). Returns nullptr when the player is stopped and no frame has been decoded. Draw it with SpriteBatch::Draw(*frame, ...). |
Properties
| Property | Type | Description |
|---|---|---|
getStateProperty() |
MediaState |
Current playback state: MediaState::Playing, MediaState::Paused, or MediaState::Stopped |
getIsLoopedProperty() / setIsLoopedProperty(bool) |
bool |
Whether the video restarts automatically when it reaches the end. Default: false. |
getIsMutedProperty() / setIsMutedProperty(bool) |
bool |
Mutes the audio track without affecting the decoded video frames. |
getVolumeProperty() / setVolumeProperty(float) |
float |
Audio volume in the range 0.0 (silent) to 1.0 (full volume). |
getPlayPositionProperty() |
System::TimeSpan |
Current playback position within the video. Read-only. |
getVideoProperty() |
Video* |
The video currently loaded into the player. A CNAEXT method, SetAudioTrackEXT(int), selects among multiple audio tracks. |
Rendering Pattern
VideoPlayer decodes frames on demand rather than pre-decoding the entire video. On each call to GetTexture(), CNA checks whether the elapsed playback time requires a new frame to be decoded, converts the YUV output to RGBA, and uploads it to the GPU as a Texture2D. This means the texture pointer returned by GetTexture() is stable for the lifetime of the player, but its contents are updated in-place each frame.
The recommended pattern inside Draw():
- Call
player.GetTexture()(aVideoPlayermember) to obtain the current frame as aTexture2D*. - If the pointer is non-null, draw it with
SpriteBatch::Draw()scaled to the desired screen rectangle.
Platform Availability
| Platform | CNA_ENABLE_VIDEO=AUTO result |
Video at run time | Recommended alternative |
|---|---|---|---|
| Linux (desktop) | FFmpeg backend if all four dev packages are found, else fallback | Decodes with the backend; NotSupportedException in a fallback build |
— |
| macOS (desktop) | Same (Homebrew pkg-config) | Decodes with the backend (the optional split was not run on macOS by its author) | — |
| Windows (MinGW-w64 and native MSVC) | Fallback (ON is a configure error) |
Throws NotSupportedException for file-backed video |
None — no video decoding on any Windows build |
| Emscripten / WebAssembly | Fallback (ON is a configure error) |
Throws | HTML5 <video> element |
| Android, iOS | Fallback (ON is a configure error) |
Throws | Android MediaPlayer API; the platform video player on iOS |
FFmpeg Setup
FFmpeg is an optional dependency, located through pkg-config at configure time and controlled by CNA_ENABLE_VIDEO. It must be installed on the host system before building CNA if you want video decoding:
| Value | Configure behaviour | Runtime video behaviour |
|---|---|---|
AUTO (default) | Enables the backend only when all four required FFmpeg pkg-config modules are found on a supported target; otherwise falls back silently. | Decodes when found; otherwise like OFF. |
ON | Requires a supported target and all four modules; configuration fails if either is missing. | Decodes through FFmpeg. |
OFF | Does not probe or link FFmpeg. | File-backed video throws NotSupportedException. |
On Debian and Ubuntu, the four development packages the build probes for are:
sudo apt install libavcodec-dev libavformat-dev libavutil-dev libswresample-dev
The decoder is built on libavcodec (codec implementations), libavformat (container demuxers), libavutil and libswresample (audio resampling). A game that never plays video can say so explicitly and drop the dependency:
cmake -S . -B build -DCNA_ENABLE_VIDEO=OFF
That build has no FFmpeg link edge at all; using Game or audio-only MediaPlayer does not bring FFmpeg into the final executable, and MediaLibrary reports an unknown (zero) duration for tracks because audio-duration probing also needs the backend. When the backend is present, CNA defines CNA_VIDEO_AVAILABLE (and the older CNA_FFMPEG_AVAILABLE) on its targets.
There is no Windows FFmpeg setup to describe. The backend is not wired up on any Windows target, so installing FFmpeg development packages there will not enable video playback, and -DCNA_ENABLE_VIDEO=ON is rejected at configure time.
Code Examples
Loading and playing a video
// Game member variables
std::optional<Video> m_introVideo; // Video has no default constructor
VideoPlayer m_videoPlayer;
// LoadContent()
void MyGame::LoadContent() {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
try {
m_introVideo.emplace(getContentProperty().Load<Video>("videos/intro"));
m_videoPlayer.setIsLoopedProperty(false);
m_videoPlayer.setVolumeProperty(1.0f);
m_videoPlayer.Play(&*m_introVideo);
} catch (const System::NotSupportedException&) {
// This build has no FFmpeg video backend (or the platform cannot decode): skip the intro.
m_introVideo.reset();
}
}
Getting the current frame and drawing with SpriteBatch
// Draw()
void MyGame::Draw(const GameTime& gameTime) {
getGraphicsDeviceProperty().Clear(Color::Black);
if (Texture2D* frame = m_videoPlayer.GetTexture()) {
const Viewport vp = getGraphicsDeviceProperty().getViewportProperty();
spriteBatch_->Begin();
spriteBatch_->Draw(
*frame,
Rectangle(0, 0, vp.getWidthProperty(), vp.getHeightProperty()),
Color::White);
spriteBatch_->End();
}
}
Checking playback state and looping
// Update()
void MyGame::Update(GameTime& gameTime) {
if (!m_introVideo || m_videoPlayer.getStateProperty() == MediaState::Stopped) {
// Intro finished (or unavailable) — transition to the main menu
LoadMainMenu();
return;
}
// Display elapsed / total time
System::TimeSpan pos = m_videoPlayer.getPlayPositionProperty();
System::TimeSpan total = m_introVideo->getDurationProperty();
UpdateProgressBar(pos, total);
}
// Enable looping before playback begins
m_videoPlayer.setIsLoopedProperty(true);
m_videoPlayer.Play(&*m_backgroundVideo);
Pausing and resuming on input
// Update()
void MyGame::Update(GameTime& gameTime) {
auto kb = Keyboard::GetState();
if (kb.IsKeyDown(Keys::Space) && m_prevKeyboard.IsKeyUp(Keys::Space)) {
if (m_videoPlayer.getStateProperty() == MediaState::Playing) {
m_videoPlayer.Pause();
} else if (m_videoPlayer.getStateProperty() == MediaState::Paused) {
m_videoPlayer.Resume();
}
}
if (kb.IsKeyDown(Keys::Escape) && m_prevKeyboard.IsKeyUp(Keys::Escape)) {
m_videoPlayer.Stop();
}
m_prevKeyboard = kb;
}
For a complete walkthrough, including a compile-time guard with CNA_VIDEO_AVAILABLE, see Tutorial 121: Video Playback.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Songs, the media library and video: the public contract — The caller-visible contract of CNA's Song, MediaLibrary, MediaPlayer, visualization data and VideoPlayer, including where file extensions and decoders disagree and how video availability changed.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-143: VideoPlayer and Video keep raw pointers to each other, so destroying a Video before its player is stopped writes to freed memory — VideoPlayer stores a borrowed Video* and Video a borrowed VideoPlayer* parent; CloseDecoder, run by Stop, Dispose and the destructor, writes through the stored Video*, and no header states the ordering rule.
- CNA-BUG-144: VideoPlayer::GetTexture lets the decoder's std::runtime_error escape, undocumented, and leaves the player Playing — A decode, packet, resample or I/O error mid-stream is thrown by VideoDecoder::NextFrame as std::runtime_error through GetTexture, whose header documents only ObjectDisposedException, and the player stays Playing so the n
- CNA-BUG-145: VideoDecoder::SetVideoStream switches to another video stream mid-playback without seeking, so decoding resumes at a non-keyframe position — A genuine SetVideoTrackEXT switch during playback opens a fresh codec context at the demuxer's current position without a seek or flush; CNA's own comment explains that such a context fails on the next non-keyframe.
- CNA-BUG-146: VideoDecoder converts every YUV frame with one full-range BT.601 matrix: limited-range and BT.709 video decode with wrong levels and colours — The planar and NV12 converters ignore the frame's colour range and matrix, so ordinary limited-range video keeps black at 16 and white at 235, and BT.709 content is decoded with BT.601 coefficients.
- CNA-BUG-186: CNA_ENABLE_VIDEO=AUTO accepts any pkg-config FFmpeg, but VideoDecoder.cpp needs the FFmpeg 5.1 channel-layout API — The FFmpeg probe states no minimum version while VideoDecoder.cpp uses AVCodecContext::ch_layout and swr_alloc_set_opts2, so a host with older FFmpeg development packages enables video at configure time and then fails to