use std::ffi::CString;
use std::fs::{ self, File, OpenOptions };
use std::os::raw::{ c_int, c_ulong };
use std::os::unix::io::AsRawFd;
use std::path::Path;
use std::ptr;
use std::sync::Mutex;
use wpe_webkit_sys::*;
use super::content_filter::user_content_manager;
use crate::types::VideoCodec;
const POLICY_JS: &str = include_str!( "codec_policy.js" );
impl VideoCodec
{
fn input_formats( self ) -> &'static [ u32 ]
{
match self
{
VideoCodec::H264 => &H264_FORMATS,
VideoCodec::Hevc => &HEVC_FORMATS,
VideoCodec::Vp8 => &VP8_FORMATS,
VideoCodec::Vp9 => &VP9_FORMATS,
VideoCodec::Av1 => &AV1_FORMATS,
}
}
}
const fn fourcc( code: &[ u8; 4 ] ) -> u32
{
( code[ 0 ] as u32 ) | ( code[ 1 ] as u32 ) << 8 | ( code[ 2 ] as u32 ) << 16 | ( code[ 3 ] as u32 ) << 24
}
const H264_FORMATS: [ u32; 3 ] = [ fourcc( b"S264" ), fourcc( b"H264" ), fourcc( b"AVC1" ) ];
const HEVC_FORMATS: [ u32; 2 ] = [ fourcc( b"S265" ), fourcc( b"HEVC" ) ];
const VP8_FORMATS: [ u32; 2 ] = [ fourcc( b"VP8F" ), fourcc( b"VP80" ) ];
const VP9_FORMATS: [ u32; 2 ] = [ fourcc( b"VP9F" ), fourcc( b"VP90" ) ];
const AV1_FORMATS: [ u32; 2 ] = [ fourcc( b"AV1F" ), fourcc( b"AV01" ) ];
#[ repr( C ) ]
struct FormatDescription
{
index: u32,
buffer_type: u32,
flags: u32,
description: [ u8; 32 ],
pixel_format: u32,
mbus_code: u32,
reserved: [ u32; 3 ],
}
#[ repr( C ) ]
struct Capability
{
driver: [ u8; 16 ],
card: [ u8; 32 ],
bus_info: [ u8; 32 ],
version: u32,
capabilities: u32,
device_caps: u32,
reserved: [ u32; 3 ],
}
const VIDIOC_QUERYCAP: c_ulong = 0x8068_5600;
const VIDIOC_ENUM_FMT: c_ulong = 0xC040_5602;
const BUF_TYPE_VIDEO_OUTPUT: u32 = 2;
const BUF_TYPE_VIDEO_OUTPUT_MPLANE: u32 = 10;
const MAX_FORMATS: u32 = 64;
extern "C"
{
fn ioctl( fd: c_int, request: c_ulong, ... ) -> c_int;
}
fn open_device( path: &Path ) -> Option<File>
{
OpenOptions::new().read( true ).write( true ).open( path ).or_else( |_| File::open( path ) ).ok()
}
const BROKEN_FORMATS: [ ( &str, &[ u32 ] ); 1 ] = [ ( "rkvdec", &VP9_FORMATS ) ];
fn driver_name( device: &File ) -> String
{
let mut capability = Capability { driver: [ 0; 16 ], card: [ 0; 32 ], bus_info: [ 0; 32 ], version: 0, capabilities: 0, device_caps: 0, reserved: [ 0; 3 ] };
let result = unsafe { ioctl( device.as_raw_fd(), VIDIOC_QUERYCAP, &mut capability as *mut Capability ) };
if result != 0 { return String::new(); }
let length = capability.driver.iter().position( |byte| *byte == 0 ).unwrap_or( capability.driver.len() );
String::from_utf8_lossy( &capability.driver[ ..length ] ).into_owned()
}
fn usable_formats( driver: &str, formats: Vec<u32> ) -> Vec<u32>
{
let broken: Vec<u32> = BROKEN_FORMATS.iter().filter( |( name, _ )| *name == driver ).flat_map( |( _, formats )| formats.iter().copied() ).collect();
formats.into_iter().filter( |format| !broken.contains( format ) ).collect()
}
fn input_formats( path: &Path ) -> Vec<u32>
{
let Some( device ) = open_device( path ) else { return Vec::new() };
let mut found = Vec::new();
for buffer_type in [ BUF_TYPE_VIDEO_OUTPUT, BUF_TYPE_VIDEO_OUTPUT_MPLANE ]
{
for index in 0..MAX_FORMATS
{
let mut description = FormatDescription { index, buffer_type, flags: 0, description: [ 0; 32 ], pixel_format: 0, mbus_code: 0, reserved: [ 0; 3 ] };
let result = unsafe { ioctl( device.as_raw_fd(), VIDIOC_ENUM_FMT, &mut description as *mut FormatDescription ) };
if result != 0 { break; }
found.push( description.pixel_format );
}
}
usable_formats( &driver_name( &device ), found )
}
pub fn hardware_video_decoders() -> Vec<VideoCodec>
{
let Ok( entries ) = fs::read_dir( "/dev" ) else { return Vec::new() };
let mut formats: Vec<u32> = Vec::new();
for entry in entries.flatten()
{
if entry.file_name().to_string_lossy().starts_with( "video" )
{
formats.extend( input_formats( &entry.path() ) );
}
}
VideoCodec::ALL.iter().copied().filter( |codec| codec.input_formats().iter().any( |format| formats.contains( format ) ) ).collect()
}
fn script_source( blocked: &[ VideoCodec ] ) -> String
{
let list: Vec<String> = blocked.iter().map( |codec| format!( "'{}'", codec.key() ) ).collect();
format!( "{}( [{}] );\n", POLICY_JS.trim_end(), list.join( "," ) )
}
static POLICY_SCRIPT: Mutex<usize> = Mutex::new( 0 );
pub fn set_blocked_video_codecs( blocked: &[ VideoCodec ] )
{
let Ok( mut held ) = POLICY_SCRIPT.lock() else { return };
unsafe
{
let manager = user_content_manager();
let old = std::mem::replace( &mut *held, 0 ) as *mut WebKitUserScript;
if !old.is_null()
{
webkit_user_content_manager_remove_script( manager, old );
webkit_user_script_unref( old );
}
if blocked.is_empty() { return; }
let Ok( source ) = CString::new( script_source( blocked ) ) else { return };
let script = webkit_user_script_new(
source.as_ptr(),
WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
ptr::null(),
ptr::null(),
);
if script.is_null() { return; }
webkit_user_content_manager_add_script( manager, script );
*held = script as usize;
}
}
#[ cfg( test ) ]
mod tests
{
use super::*;
#[ test ]
fn fourcc_matches_the_v4l2_values()
{
assert_eq!( fourcc( b"S264" ), 0x3436_3253 );
assert_eq!( fourcc( b"VP9F" ), 0x4639_5056 );
assert_eq!( fourcc( b"AV1F" ), 0x4631_5641 );
}
#[ test ]
fn format_description_has_the_kernel_layout()
{
assert_eq!( std::mem::size_of::<FormatDescription>(), 64 );
let ioctl_number = ( 3u32 << 30 ) | ( 64 << 16 ) | ( ( b'V' as u32 ) << 8 ) | 2;
assert_eq!( VIDIOC_ENUM_FMT as u32, ioctl_number );
}
#[ test ]
fn capability_has_the_kernel_layout()
{
assert_eq!( std::mem::size_of::<Capability>(), 104 );
let ioctl_number = ( 2u32 << 30 ) | ( 104 << 16 ) | ( ( b'V' as u32 ) << 8 );
assert_eq!( VIDIOC_QUERYCAP as u32, ioctl_number );
}
#[ test ]
fn rkvdec_does_not_offer_vp9()
{
let formats = vec![ fourcc( b"S264" ), fourcc( b"VP9F" ), fourcc( b"S265" ) ];
assert_eq!( usable_formats( "rkvdec", formats.clone() ), vec![ fourcc( b"S264" ), fourcc( b"S265" ) ] );
assert_eq!( usable_formats( "hantro-vpu", formats.clone() ), formats );
}
#[ test ]
fn every_codec_has_input_formats()
{
for codec in VideoCodec::ALL
{
assert!( !codec.input_formats().is_empty() );
}
}
#[ test ]
fn script_passes_the_blocked_codecs_to_the_policy()
{
let source = script_source( &[ VideoCodec::Av1, VideoCodec::Vp9 ] );
assert!( source.ends_with( "} )( ['av1','vp9'] );\n" ) );
assert!( !source.contains( "var " ) );
assert!( !source.contains( '\0' ) );
assert!( source.contains( "const refuse" ) );
}
}