Recursive AI

Check if a browser can play a video in C#

All onemedia recipes

Media.CanPlay(path, profile) reads a file’s container and codec headers and says whether a browser plays it, as a downloaded file or through Media Source Extensions. When it doesn’t, the verdict lists each blocker with a code, the track it concerns and the fix: a remux, an audio re-encode, or nothing this library can do (the video codec). The inputs are MDN’s CC0 flower clip as MP4 (H.264/AAC) and WebM (VP8/Vorbis), the Sintel trailer (CC BY 3.0, © copyright Blender Foundation | durian.blender.org) as MP4 and copied into AVI with FFmpeg, and test5.mkv from the Matroska project’s test files (cut from Elephants Dream, CC BY: © copyright 2006, Blender Foundation / Netherlands Media Art Institute / www.elephantsdream.org).

Terminal
dotnet new console -o CanPlay && cd CanPlay
dotnet add package RecursiveAI.OneMedia --version 0.3.0-preview.1
m=https://interactive-examples.mdn.mozilla.net/media/cc0-videos
curl -L -o flower.mp4 $m/flower.mp4
curl -L -o flower.webm $m/flower.webm
curl -L -o sintel.mp4 https://download.blender.org/durian/trailer/sintel_trailer-480p.mp4
curl -L -o test5.mkv https://raw.githubusercontent.com/ietf-wg-cellar/matroska-test-files/master/test_files/test5.mkv
ffmpeg -v error -i sintel.mp4 -c copy sintel.avi

Put this in Program.cs and run dotnet run:

C#
using RecursiveAI.OneMedia;
using RecursiveAI.OneMedia.Playback;
 
string[] files = ["flower.mp4", "flower.webm", "sintel.mp4", "test5.mkv", "sintel.avi"];
BrowserProfile[] profiles = [BrowserProfiles.Chrome, BrowserProfiles.Chromium, BrowserProfiles.Firefox,
                             BrowserProfiles.Safari, BrowserProfiles.GenericMp4];
 
foreach (string path in files)
{
    Console.WriteLine(path);
    foreach (BrowserProfile profile in profiles)
    {
        PlaybackVerdict verdict = Media.CanPlay(path, profile);
        string answer = verdict.CanPlay ? "plays"
            : "no: " + string.Join(", ", verdict.Blockers.Select(b => $"{b.Code} (track {b.TrackIndex}, {b.Remedy})"));
        string warnings = string.Concat(verdict.Progressive.Warnings.Select(w => $" [warning {w.Code}]"));
        Console.WriteLine($"  {profile.Id,-12} {answer}{warnings}");
    }
}
Text
flower.mp4
  chrome       plays
  chromium     no: codec.unsupported (track 0, VideoBlocker), codec.unsupported (track 1, TranscodeAudio)
  firefox      plays
  safari       plays
  generic-mp4  plays
flower.webm
  chrome       plays
  chromium     plays
  firefox      plays
  safari       plays
  generic-mp4  no: container.unsupported (track -1, Remux), codec.unsupported (track 0, VideoBlocker), codec.unsupported (track 1, TranscodeAudio)
sintel.mp4
  chrome       plays [warning progressive.movie-at-end]
  chromium     no: codec.unsupported (track 0, VideoBlocker), codec.unsupported (track 1, TranscodeAudio) [warning progressive.movie-at-end]
  firefox      plays [warning progressive.movie-at-end]
  safari       plays [warning progressive.movie-at-end]
  generic-mp4  plays [warning progressive.movie-at-end]
test5.mkv
  chrome       plays
  chromium     no: codec.unsupported (track 0, VideoBlocker), codec.unsupported (track 1, TranscodeAudio), codec.unsupported (track 8, TranscodeAudio)
  firefox      no: container.unsupported (track -1, Remux), codec.container (track 0, Remux), codec.container (track 1, Remux), codec.container (track 8, Remux)
  safari       no: container.unsupported (track -1, Remux), codec.container (track 0, Remux), codec.container (track 1, Remux), codec.container (track 8, Remux)
  generic-mp4  no: container.unsupported (track -1, Remux), codec.container (track 0, Remux), codec.container (track 1, Remux), codec.container (track 8, Remux)
sintel.avi
  chrome       no: container.unsupported (track -1, Remux), codec.container (track 0, Remux), codec.container (track 1, Remux)
  chromium     no: container.unsupported (track -1, Remux), codec.unsupported (track 0, VideoBlocker), codec.unsupported (track 1, TranscodeAudio)
  firefox      no: container.unsupported (track -1, Remux), codec.container (track 0, Remux), codec.container (track 1, Remux)
  safari       no: container.unsupported (track -1, Remux), codec.container (track 0, Remux), codec.container (track 1, Remux)
  generic-mp4  no: container.unsupported (track -1, Remux), codec.container (track 0, Remux), codec.container (track 1, Remux)

Track -1 is the container. Remux means the same packets play in another container; TranscodeAudio, that an audio track has to be re-encoded; VideoBlocker, that the video codec itself doesn’t play there, which OneMedia never re-encodes. verdict.CanBeMadePlayable is true when no blocker is a video blocker, and Make a video play in browsers applies the fixes. b.Message has a sentence for each blocker, such as “Chromium does not play h264.”

The Sintel MP4 plays everywhere but carries the warning progressive.movie-at-end: its index is at the end, so a browser has to fetch the end of the file before it starts. MP4 faststart fixes that. Warnings never block.

What a profile means

The profiles are static data, not a browser probe at run time. The project’s playability page defines them:

  • chrome: Chrome on Windows, macOS, Linux (proprietary codecs present). Evidence: verified: Chrome 156.0.8078.74, macOS arm64.
  • chromium: Chromium built without proprietary codecs (also most Linux distribution builds, embedded browsers). Evidence: verified: Chromium 140.0.7339.16, Linux x64.
  • firefox: Firefox where the OS or system libraries supply H.264, HEVC and AAC. Evidence: verified: Firefox 141.0, Linux x64, system FFmpeg libraries.
  • safari: the WebKit engine on macOS. Evidence: verified: WebKit 26.0 (Playwright build), macOS arm64.
  • webkit-linux: WebKit (WPE) with GStreamer and its FFmpeg plug-ins. Evidence: verified: WebKit 26.0 (Playwright WPE build), Linux x64.
  • ios-safari: Safari and every iOS browser. Evidence: public documentation only.
  • android-chrome: Chrome on Android. Evidence: public documentation only.
  • generic-mp4: what all of the above accept: H.264 Baseline/Main/High level <= 4.2, 4-2-0 8-bit avc1, AAC-LC, MP3. Evidence: derived: the intersection.

The project checked the five verified profiles by playing 75 files (16 also through Media Source Extensions) in Chromium 140, Firefox 141 and WebKit 26: the verdict equals what was observed in all 455 cases. Its safari row was observed in Playwright’s macOS WebKit build, which shares Safari’s media stack but isn’t Safari.

Checked in headless Chrome

The chrome column was checked for this page in headless Chrome 149 on Linux (a build with H.264 and AAC). Each file was served over HTTP with range requests and loaded into a <video> element; canPlayType got the type and the codecs string OneMedia derives, the video was played for 2.5 s, and the audio level was measured through the Web Audio API:

Text
file         canPlayType  readyState  duration  played to  frames  audio peak
flower.mp4   probably     4           5.055 s   2.51 s     79      0
flower.webm  probably     4           5.059 s   2.51 s     80      0
sintel.mp4   probably     4           52.208 s  2.50 s     64      0.088
test5.mkv    probably     4           46.665 s  2.52 s     65      0.634
sintel.avi   ""           0           —         error 4    0       0

Chrome played the four files the chrome profile says it plays, and refused the AVI as the verdict says: error 4, MEDIA_ERR_SRC_NOT_SUPPORTED (“DEMUXER_ERROR_COULD_NOT_OPEN”), with canPlayType("video/x-msvideo") empty. The flower clip’s audio level is 0 because its audio track is silent (FFmpeg measures −91 dB throughout). The Chromium, Firefox and Safari columns were not run for this page; they rest on the project’s checks above.

Limits

  • Headers only. A verdict reads the container header and codec configuration. It doesn’t scan samples, so a file with badly ordered blocks or a feature its header doesn’t announce can still fail; hardware decoders, DRM, codec levels enforced from the real frame rate and autoplay policy are outside it.
  • Unverified profiles. ios-safari and android-chrome come from MDN’s codec guides and Chrome’s and Apple’s public media notes, not from devices. The chrome profile was verified on Chrome 156; other versions can differ.
  • Rows no test file exercises (Vorbis in MP4 for Chromium, HEVC in MOV) are left out of a profile, which makes the verdict “blocked”.
  • AVI and other remux candidates. A Remux remedy for an AVI holding avc1 H.264, as here, doesn’t hold in 0.3.0-preview.1: the remux drops that video track. See the limits of Make a video play in browsers.

To get the RFC 6381 codecs string for a <source type> attribute, see Get video duration, resolution and codec.