Problem:
Currently, TimedMediaHandler incorrectly handles AV1 codec strings in WebMHandler.php. The codec string for AV1 is passed as lowercase (av1), which is not compliant with browser and VideoJS requirements. AV1 codec strings are case-sensitive and must follow the format defined by the AV1 specification, such as av01.<profile>.<level><tier>.<bitDepth>.
This issue prevents original AV1 video files uploaded to MediaWiki from being played through the VideoJS player. Instead, users see the error:
"No compatible source was found for this media."
Steps to Reproduce:
- Upload an AV1 video file
- Check the MIME type generated by getWebType() in WebMHandler.php.
Current output: video/webm; codecs="av1, opus"
Correct output: video/webm; codecs="av01.0.05M.08, opus"
- Attempt to play the video in VideoJS or a browser.
Without this fix, playback fails with the error: "No compatible source was found for this media.". There is also no way to watch the video source in Videojs.
Proposed Solution (How I solved the problem in my local wiki:):
The following changes have been made to the WebMHandler.php file:
- Preserve case sensitivity for AV1 codec strings:
A custom function (getAV1CodecString) was implemented to dynamically generate the correct codec string based on the video metadata.
- Dynamic generation of AV1 codec string:
The getAV1CodecString function analyzes video metadata (e.g., resolution, bit depth, frame rate) to construct a codec string that complies with the AV1 specification.
For example: A 1080p video at 30fps, 8-bit depth is assigned av01.0.05M.08. A 4K video at 60fps, 10-bit depth is assigned av01.0.06H.10 etc
- Case-sensitive handling in getWebType:
Only the AV1 codec string retains its case-sensitive format. All other codecs (e.g., VP8, VP9, Opus) are converted to lowercase for consistency.
/**
* Returns the MIME type for the file, including codecs information.
*
* @param File $file The file object.
* @return string The MIME type including the codecs.
*/
public function getWebType( $file ) {
// Determine the base type (audio or video) based on file dimensions
$baseType = ( $file->getWidth() === 0 && $file->getHeight() === 0 ) ? 'audio' : 'video';
// Get the stream types (codecs) from the file metadata
$streams = $this->getStreamTypes( $file );
if ( !$streams ) {
// Return the base type with WebM format if no streams are found
return $baseType . '/webm';
}
// Process codecs: Keep AV1 in original case, convert others to lowercase
$processedStreams = array_map( function ( $codec ) {
return stripos( $codec, 'av01.' ) === 0 ? $codec : strtolower( $codec );
}, $streams );
// Combine processed streams into a single codec string
$codecs = implode( ', ', $processedStreams );
return $baseType . '/webm; codecs="' . $codecs . '"';
}
/**
* Generates a codec string for AV1 based on the metadata.
*
* @param array $metadata Video metadata.
* @return string The codec string for AV1.
*/
private function getAV1CodecString( $metadata ) {
// Default values in case metadata is incomplete
$profile = '0'; // Default: Main Profile
$level = '05'; // Default: Level 5.0
$tier = 'M'; // Default: Main Tier
$bitDepth = '08'; // Default: 8-bit
// Check for bit depth
if ( isset( $metadata['video']['bit_depth'] ) ) {
if ( $metadata['video']['bit_depth'] === 10 ) {
$bitDepth = '10'; // 10-bit depth
} elseif ( $metadata['video']['bit_depth'] === 12 ) {
$bitDepth = '12'; // 12-bit depth (rare)
}
}
// Analyze resolution and adjust level
if ( isset( $metadata['video']['resolution_x'] ) && isset( $metadata['video']['resolution_y'] ) ) {
$width = $metadata['video']['resolution_x'];
$height = $metadata['video']['resolution_y'];
// Determine level based on resolution and frame rate
if ( isset( $metadata['video']['frame_rate'] ) ) {
$frameRate = $metadata['video']['frame_rate'];
} else {
$frameRate = 30; // Assume a default frame rate if not provided
}
$pixelsPerSecond = $width * $height * $frameRate;
// Assign level based on the AV1 specification (simplified mapping)
if ( $pixelsPerSecond <= 829440 ) { // <= 720p@30fps
$level = '02';
} elseif ( $pixelsPerSecond <= 2073600 ) { // <= 1080p@30fps
$level = '03';
} elseif ( $pixelsPerSecond <= 3686400 ) { // <= 1080p@60fps or 1440p@30fps
$level = '04';
} elseif ( $pixelsPerSecond <= 7372800 ) { // <= 4K@30fps
$level = '05';
} else {
$level = '06'; // Higher resolutions or frame rates
}
}
// Check for tier, assume Main Tier by default
if ( isset( $metadata['video']['tier'] ) && $metadata['video']['tier'] === 'high' ) {
$tier = 'H'; // High Tier
}
// Construct the codec string
return "av01.$profile.$level$tier.$bitDepth";
}
/**
* @param File $file
* @return string[]|false
*/
public function getStreamTypes( $file ) {
$streamTypes = [];
$metadata = $this->unpackMetadata( $file->getMetadata() );
if ( !$metadata || isset( $metadata['error'] ) ) {
return false;
}
// id3 gives 'V_VP8' for what we call VP8
if ( isset( $metadata['video'] ) ) {
if ( $metadata['video']['dataformat'] === 'vp8' ) {
$streamTypes[] = 'VP8';
} elseif ( $metadata['video']['dataformat'] === 'vp9'
|| $metadata['video']['dataformat'] === 'V_VP9'
) {
// Currently getID3 calls it V_VP9. That will probably change to vp9
// once getID3 actually gets support for the codec.
$streamTypes[] = 'VP9';
} elseif ( $metadata['video']['dataformat'] === 'V_AV1' ) {
$streamTypes[] = $this->getAV1CodecString( $metadata );
}
}
if ( isset( $metadata['audio'] ) ) {
if ( $metadata['audio']['dataformat'] === 'vorbis' ) {
$streamTypes[] = 'Vorbis';
} elseif ( $metadata['audio']['dataformat'] === 'opus'
|| $metadata['audio']['dataformat'] === 'A_OPUS'
) {
// Currently getID3 calls it A_OPUS. That will probably change to 'opus'
// once getID3 actually gets support for the codec.
$streamTypes[] = 'opus';
}
}
return $streamTypes;
}This issue might appear like a feature request, but it is fundamentally a bug since it breaks playback for AV1 files uploaded to MediaWiki. Correct MIME types are a requirement for browser and VideoJS compatibility.