use crate::{Image, Pt, Texture};
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use std::cell::RefCell;
use std::fmt;
use std::path::PathBuf;
use std::time::Duration;
#[cfg(target_os = "android")]
#[path = "video_android.rs"]
mod android_backend;
#[cfg(any(target_os = "ios", target_os = "macos"))]
#[path = "video_ios.rs"]
mod apple_backend;
#[cfg(target_os = "windows")]
#[path = "video_windows.rs"]
mod windows_backend;
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
#[derive(Debug)]
pub(crate) struct ExternalVideoCopy {
pub(crate) texture_id: u32,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) video: web_sys::HtmlVideoElement,
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
#[derive(Default)]
pub(crate) struct VideoCopyQueue {
pub(crate) copies: RefCell<Vec<ExternalVideoCopy>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MediaSource {
File(PathBuf),
Url(String),
Asset(String),
}
impl MediaSource {
pub fn file(path: impl Into<PathBuf>) -> Self {
Self::File(path.into())
}
pub fn url(url: impl Into<String>) -> Self {
Self::Url(url.into())
}
pub fn asset(path: impl Into<String>) -> Self {
Self::Asset(path.into())
}
#[cfg(any(
target_os = "android",
target_os = "ios",
target_os = "macos",
all(target_arch = "wasm32", target_os = "unknown")
))]
fn uri(&self) -> String {
match self {
Self::File(path) => path.to_string_lossy().into_owned(),
Self::Url(url) | Self::Asset(url) => url.clone(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VideoState {
Closed,
Opening,
Ready,
Playing,
Paused,
Ended,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VideoEvent {
Opened,
Started,
Paused,
Stopped,
Ended,
FrameReady,
Error(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VideoError {
UnsupportedPlatform,
InvalidState(&'static str),
Backend(String),
}
impl fmt::Display for VideoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedPlatform => {
f.write_str("video playback is not available on this platform")
}
Self::InvalidState(message) => f.write_str(message),
Self::Backend(message) => f.write_str(message),
}
}
}
impl std::error::Error for VideoError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct VideoTexture {
texture: Texture,
}
impl VideoTexture {
pub fn image(self) -> Image {
self.texture.view()
}
pub fn texture(self) -> Texture {
self.texture
}
pub fn width(self) -> Pt {
self.texture.width()
}
pub fn height(self) -> Pt {
self.texture.height()
}
pub fn is_ready(self, ctx: &crate::Context) -> bool {
self.texture.is_ready(ctx)
}
}
impl From<VideoTexture> for Image {
fn from(value: VideoTexture) -> Self {
value.image()
}
}
pub struct VideoPlayer {
source: MediaSource,
backend: VideoBackend,
state: VideoState,
texture: Option<VideoTexture>,
duration: Option<Duration>,
position: Duration,
looping: bool,
muted: bool,
volume: f32,
playback_rate: f64,
events: Vec<VideoEvent>,
}
impl fmt::Debug for VideoPlayer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VideoPlayer")
.field("source", &self.source)
.field("state", &self.state)
.field("texture", &self.texture)
.field("duration", &self.duration)
.field("position", &self.position)
.field("looping", &self.looping)
.finish()
}
}
impl VideoPlayer {
pub fn new(source: MediaSource) -> Self {
Self {
source,
backend: VideoBackend::Unsupported,
state: VideoState::Closed,
texture: None,
duration: None,
position: Duration::ZERO,
looping: false,
muted: false,
volume: 1.0,
playback_rate: 1.0,
events: Vec::new(),
}
}
pub fn source(&self) -> &MediaSource {
&self.source
}
pub fn state(&self) -> VideoState {
self.state
}
pub fn duration(&self) -> Option<Duration> {
self.duration
}
pub fn position(&self) -> Duration {
self.position
}
pub fn texture(&self) -> Option<VideoTexture> {
self.texture
}
pub fn open(&mut self) -> Result<(), VideoError> {
self.backend.close();
self.backend = VideoBackend::Unsupported;
self.backend = VideoBackend::open(&self.source)?;
self.texture = None;
self.duration = None;
self.position = Duration::ZERO;
self.transition(VideoState::Opening);
Ok(())
}
pub fn close(&mut self) {
self.backend.close();
self.backend = VideoBackend::Unsupported;
self.texture = None;
self.duration = None;
self.position = Duration::ZERO;
self.transition(VideoState::Closed);
}
pub fn update(&mut self, ctx: &mut crate::Context) -> Result<(), VideoError> {
let snapshot = self.backend.update()?;
self.duration = snapshot.duration.or(self.duration);
self.position = snapshot.position;
if let Some((width, height)) = snapshot.dimensions
&& self.texture.is_none()
{
let rgba = vec![0; width as usize * height as usize * 4];
let texture = ctx.register_streaming_texture(
width,
height,
Pt::from(width as f32),
Pt::from(height as f32),
&rgba,
);
self.texture = Some(VideoTexture { texture });
}
if let Some(rgba) = snapshot.rgba
&& let Some(video_texture) = self.texture
{
ctx.update_texture_rgba8(video_texture.texture, rgba)
.map_err(|error| VideoError::Backend(error.to_string()))?;
self.events.push(VideoEvent::FrameReady);
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
if let Some(video) = snapshot.external_video
&& let Some(video_texture) = self.texture
{
ctx.queue_external_video_copy(video_texture.texture, video);
self.events.push(VideoEvent::FrameReady);
}
self.transition(snapshot.state);
Ok(())
}
pub fn play(&mut self) -> Result<(), VideoError> {
self.backend.play()?;
self.transition(VideoState::Playing);
Ok(())
}
pub fn pause(&mut self) -> Result<(), VideoError> {
self.backend.pause()?;
self.transition(VideoState::Paused);
Ok(())
}
pub fn stop(&mut self) -> Result<(), VideoError> {
self.backend.stop()?;
self.position = Duration::ZERO;
self.transition(VideoState::Ready);
self.events.push(VideoEvent::Stopped);
Ok(())
}
pub fn seek(&mut self, position: Duration) -> Result<(), VideoError> {
self.backend.seek(position)?;
self.position = position;
Ok(())
}
pub fn set_loop(&mut self, looping: bool) -> Result<(), VideoError> {
self.looping = looping;
self.backend.set_loop(looping)
}
pub fn is_looping(&self) -> bool {
self.looping
}
pub fn set_muted(&mut self, muted: bool) -> Result<(), VideoError> {
self.muted = muted;
self.backend.set_muted(muted)
}
pub fn is_muted(&self) -> bool {
self.muted
}
pub fn set_volume(&mut self, volume: f32) -> Result<(), VideoError> {
self.volume = volume.clamp(0.0, 1.0);
self.backend.set_volume(self.volume)
}
pub fn volume(&self) -> f32 {
self.volume
}
pub fn set_playback_rate(&mut self, rate: f64) -> Result<(), VideoError> {
if !rate.is_finite() || rate <= 0.0 {
return Err(VideoError::Backend(
"playback rate must be finite and positive".to_string(),
));
}
self.playback_rate = rate;
self.backend.set_playback_rate(rate)
}
pub fn playback_rate(&self) -> f64 {
self.playback_rate
}
pub fn take_events(&mut self) -> Vec<VideoEvent> {
std::mem::take(&mut self.events)
}
fn transition(&mut self, state: VideoState) {
if self.state == state {
return;
}
let old = self.state;
self.state = state;
match state {
VideoState::Ready if old == VideoState::Opening => self.events.push(VideoEvent::Opened),
VideoState::Playing if old != VideoState::Playing => {
self.events.push(VideoEvent::Started)
}
VideoState::Paused if old == VideoState::Playing => {
self.events.push(VideoEvent::Paused)
}
VideoState::Ended if old != VideoState::Ended => self.events.push(VideoEvent::Ended),
VideoState::Error => self.events.push(VideoEvent::Error(
"video backend entered an error state".to_string(),
)),
_ => {}
}
}
}
impl Drop for VideoPlayer {
fn drop(&mut self) {
self.backend.close();
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VideoPlaylistEvent {
Video { index: usize, event: VideoEvent },
Changed { index: usize },
Completed,
}
pub struct VideoPlaylist {
sources: Vec<MediaSource>,
current: Option<VideoPlayer>,
preloaded: Option<PreloadedVideo>,
display_texture: Option<VideoTexture>,
index: usize,
playing: bool,
looping: bool,
muted: bool,
volume: f32,
playback_rate: f64,
events: Vec<VideoPlaylistEvent>,
}
struct PreloadedVideo {
index: usize,
player: VideoPlayer,
}
impl fmt::Debug for VideoPlaylist {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VideoPlaylist")
.field("sources", &self.sources)
.field("current_index", &self.current_index())
.field("state", &self.state())
.field("looping", &self.looping)
.finish()
}
}
impl VideoPlaylist {
pub fn new(sources: impl IntoIterator<Item = MediaSource>) -> Self {
Self {
sources: sources.into_iter().collect(),
current: None,
preloaded: None,
display_texture: None,
index: 0,
playing: false,
looping: false,
muted: false,
volume: 1.0,
playback_rate: 1.0,
events: Vec::new(),
}
}
pub fn push(&mut self, source: MediaSource) {
self.sources.push(source);
}
pub fn sources(&self) -> &[MediaSource] {
&self.sources
}
pub fn len(&self) -> usize {
self.sources.len()
}
pub fn is_empty(&self) -> bool {
self.sources.is_empty()
}
pub fn current_index(&self) -> Option<usize> {
self.sources.get(self.index).map(|_| self.index)
}
pub fn current_source(&self) -> Option<&MediaSource> {
self.sources.get(self.index)
}
pub fn current(&self) -> Option<&VideoPlayer> {
self.current.as_ref()
}
pub fn state(&self) -> VideoState {
self.current
.as_ref()
.map(VideoPlayer::state)
.unwrap_or(VideoState::Closed)
}
pub fn texture(&self) -> Option<VideoTexture> {
self.current
.as_ref()
.and_then(VideoPlayer::texture)
.or(self.display_texture)
}
pub fn duration(&self) -> Option<Duration> {
self.current.as_ref().and_then(VideoPlayer::duration)
}
pub fn position(&self) -> Duration {
self.current
.as_ref()
.map(VideoPlayer::position)
.unwrap_or(Duration::ZERO)
}
pub fn open(&mut self) -> Result<(), VideoError> {
self.ensure_current_index()?;
self.close_players();
self.display_texture = None;
self.playing = false;
self.current = Some(self.build_player(self.index)?);
self.events
.push(VideoPlaylistEvent::Changed { index: self.index });
if let Err(error) = self.prepare_next() {
self.close_players();
return Err(error);
}
Ok(())
}
pub fn close(&mut self) {
self.close_players();
self.display_texture = None;
self.playing = false;
}
pub fn update(&mut self, ctx: &mut crate::Context) -> Result<(), VideoError> {
self.ensure_current_index()?;
if self.current.is_none() {
self.open()?;
}
let index = self.index;
let ended = {
let player = self
.current
.as_mut()
.ok_or(VideoError::InvalidState("video playlist is not open"))?;
player.update(ctx)?;
let mut ended = false;
for event in player.take_events() {
ended |= matches!(event, VideoEvent::Ended);
self.events.push(VideoPlaylistEvent::Video { index, event });
}
if let Some(texture) = player.texture() {
self.display_texture = Some(texture);
}
ended
};
self.prepare_next()?;
if let Some(preloaded) = &mut self.preloaded {
preloaded.player.update(ctx)?;
let _ = preloaded.player.take_events();
}
if ended {
self.advance_after_end()?;
}
Ok(())
}
pub fn play(&mut self) -> Result<(), VideoError> {
if self.current.is_none() {
self.open()?;
}
if self.state() == VideoState::Ended {
self.switch_to(self.next_index().unwrap_or(0), false)?;
}
self.current
.as_mut()
.ok_or(VideoError::InvalidState("video playlist is not open"))?
.play()?;
self.playing = true;
Ok(())
}
pub fn pause(&mut self) -> Result<(), VideoError> {
self.current
.as_mut()
.ok_or(VideoError::InvalidState("video playlist is not open"))?
.pause()?;
self.playing = false;
Ok(())
}
pub fn stop(&mut self) -> Result<(), VideoError> {
self.current
.as_mut()
.ok_or(VideoError::InvalidState("video playlist is not open"))?
.stop()?;
self.playing = false;
Ok(())
}
pub fn seek(&mut self, position: Duration) -> Result<(), VideoError> {
self.current
.as_mut()
.ok_or(VideoError::InvalidState("video playlist is not open"))?
.seek(position)
}
pub fn next(&mut self) -> Result<bool, VideoError> {
self.ensure_current_index()?;
let Some(target) = self.next_index() else {
return Ok(false);
};
self.switch_to(target, self.playing)?;
Ok(true)
}
pub fn previous(&mut self) -> Result<bool, VideoError> {
self.ensure_current_index()?;
let target = if self.index > 0 {
Some(self.index - 1)
} else if self.looping && !self.sources.is_empty() {
Some(self.sources.len() - 1)
} else {
None
};
let Some(target) = target else {
return Ok(false);
};
self.switch_to(target, self.playing)?;
Ok(true)
}
pub fn select(&mut self, index: usize) -> Result<(), VideoError> {
if index >= self.sources.len() {
return Err(VideoError::InvalidState(
"video playlist index is out of bounds",
));
}
self.switch_to(index, self.playing)
}
pub fn set_loop(&mut self, looping: bool) {
self.looping = looping;
let expected = self.next_index();
if self.preloaded.as_ref().map(|item| item.index) != expected
&& let Some(mut stale) = self.preloaded.take()
{
stale.player.close();
}
}
pub fn is_looping(&self) -> bool {
self.looping
}
pub fn set_muted(&mut self, muted: bool) -> Result<(), VideoError> {
self.muted = muted;
if let Some(player) = &mut self.current {
player.set_muted(muted)?;
}
if let Some(preloaded) = &mut self.preloaded {
preloaded.player.set_muted(muted)?;
}
Ok(())
}
pub fn is_muted(&self) -> bool {
self.muted
}
pub fn set_volume(&mut self, volume: f32) -> Result<(), VideoError> {
self.volume = volume.clamp(0.0, 1.0);
if let Some(player) = &mut self.current {
player.set_volume(self.volume)?;
}
if let Some(preloaded) = &mut self.preloaded {
preloaded.player.set_volume(self.volume)?;
}
Ok(())
}
pub fn volume(&self) -> f32 {
self.volume
}
pub fn set_playback_rate(&mut self, rate: f64) -> Result<(), VideoError> {
if !rate.is_finite() || rate <= 0.0 {
return Err(VideoError::Backend(
"playback rate must be finite and positive".to_string(),
));
}
self.playback_rate = rate;
if let Some(player) = &mut self.current {
player.set_playback_rate(rate)?;
}
if let Some(preloaded) = &mut self.preloaded {
preloaded.player.set_playback_rate(rate)?;
}
Ok(())
}
pub fn playback_rate(&self) -> f64 {
self.playback_rate
}
pub fn take_events(&mut self) -> Vec<VideoPlaylistEvent> {
std::mem::take(&mut self.events)
}
fn ensure_current_index(&self) -> Result<(), VideoError> {
if self.sources.is_empty() {
Err(VideoError::InvalidState("video playlist is empty"))
} else {
Ok(())
}
}
fn build_player(&self, index: usize) -> Result<VideoPlayer, VideoError> {
let mut player = VideoPlayer::new(self.sources[index].clone());
player.open()?;
player.set_loop(false)?;
player.set_muted(self.muted)?;
player.set_volume(self.volume)?;
player.set_playback_rate(self.playback_rate)?;
Ok(player)
}
fn next_index(&self) -> Option<usize> {
if self.index + 1 < self.sources.len() {
Some(self.index + 1)
} else if self.looping && !self.sources.is_empty() {
Some(0)
} else {
None
}
}
fn prepare_next(&mut self) -> Result<(), VideoError> {
let target = self.next_index();
if self.preloaded.as_ref().map(|item| item.index) == target {
return Ok(());
}
if let Some(mut stale) = self.preloaded.take() {
stale.player.close();
}
if let Some(index) = target {
self.preloaded = Some(PreloadedVideo {
index,
player: self.build_player(index)?,
});
}
Ok(())
}
fn switch_to(&mut self, index: usize, autoplay: bool) -> Result<(), VideoError> {
let mut next = if self.preloaded.as_ref().map(|item| item.index) == Some(index) {
self.preloaded
.take()
.expect("preloaded item disappeared")
.player
} else {
self.build_player(index)?
};
if autoplay && let Err(error) = next.play() {
next.close();
return Err(error);
}
if let Some(texture) = next.texture() {
self.display_texture = Some(texture);
}
if let Some(mut previous) = self.current.take() {
previous.close();
}
if let Some(mut stale) = self.preloaded.take() {
stale.player.close();
}
self.index = index;
self.current = Some(next);
self.playing = autoplay;
self.events
.push(VideoPlaylistEvent::Changed { index: self.index });
Ok(())
}
fn close_players(&mut self) {
if let Some(mut player) = self.current.take() {
player.close();
}
if let Some(mut preloaded) = self.preloaded.take() {
preloaded.player.close();
}
}
fn advance_after_end(&mut self) -> Result<(), VideoError> {
let Some(target) = self.next_index() else {
self.playing = false;
self.events.push(VideoPlaylistEvent::Completed);
return Ok(());
};
self.switch_to(target, true)
}
}
impl Drop for VideoPlaylist {
fn drop(&mut self) {
self.close_players();
}
}
struct BackendSnapshot {
state: VideoState,
duration: Option<Duration>,
position: Duration,
dimensions: Option<(u32, u32)>,
rgba: Option<Vec<u8>>,
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
external_video: Option<web_sys::HtmlVideoElement>,
}
enum VideoBackend {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
Web(WebVideoBackend),
#[cfg(target_os = "android")]
Android(android_backend::AndroidVideoBackend),
#[cfg(any(target_os = "ios", target_os = "macos"))]
Apple(apple_backend::AppleVideoBackend),
#[cfg(target_os = "windows")]
Windows(windows_backend::WindowsVideoBackend),
Unsupported,
}
impl VideoBackend {
fn open(source: &MediaSource) -> Result<Self, VideoError> {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
return Ok(Self::Web(WebVideoBackend::open(source)?));
}
#[cfg(target_os = "android")]
{
return Ok(Self::Android(android_backend::AndroidVideoBackend::open(
source,
)?));
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
{
return Ok(Self::Apple(apple_backend::AppleVideoBackend::open(source)?));
}
#[cfg(target_os = "windows")]
{
return Ok(Self::Windows(windows_backend::WindowsVideoBackend::open(
source,
)?));
}
#[cfg(not(any(
target_os = "android",
target_os = "ios",
target_os = "macos",
target_os = "windows",
all(target_arch = "wasm32", target_os = "unknown")
)))]
{
let _ = source;
Err(VideoError::UnsupportedPlatform)
}
}
fn close(&mut self) {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
if let Self::Web(backend) = self {
backend.close();
}
#[cfg(target_os = "android")]
if let Self::Android(backend) = self {
backend.close();
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
if let Self::Apple(backend) = self {
backend.close();
}
#[cfg(target_os = "windows")]
if let Self::Windows(backend) = self {
backend.close();
}
}
fn update(&mut self) -> Result<BackendSnapshot, VideoError> {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
if let Self::Web(backend) = self {
return backend.update();
}
#[cfg(target_os = "android")]
if let Self::Android(backend) = self {
return backend.update();
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
if let Self::Apple(backend) = self {
return backend.update();
}
#[cfg(target_os = "windows")]
if let Self::Windows(backend) = self {
return backend.update();
}
Err(VideoError::UnsupportedPlatform)
}
fn play(&mut self) -> Result<(), VideoError> {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
if let Self::Web(backend) = self {
return backend.play();
}
#[cfg(target_os = "android")]
if let Self::Android(backend) = self {
return backend.play();
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
if let Self::Apple(backend) = self {
return backend.play();
}
#[cfg(target_os = "windows")]
if let Self::Windows(backend) = self {
return backend.play();
}
Err(VideoError::UnsupportedPlatform)
}
fn pause(&mut self) -> Result<(), VideoError> {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
if let Self::Web(backend) = self {
return backend.pause();
}
#[cfg(target_os = "android")]
if let Self::Android(backend) = self {
return backend.pause();
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
if let Self::Apple(backend) = self {
return backend.pause();
}
#[cfg(target_os = "windows")]
if let Self::Windows(backend) = self {
return backend.pause();
}
Err(VideoError::UnsupportedPlatform)
}
fn stop(&mut self) -> Result<(), VideoError> {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
if let Self::Web(backend) = self {
return backend.stop();
}
#[cfg(target_os = "android")]
if let Self::Android(backend) = self {
return backend.stop();
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
if let Self::Apple(backend) = self {
return backend.stop();
}
#[cfg(target_os = "windows")]
if let Self::Windows(backend) = self {
return backend.stop();
}
Err(VideoError::UnsupportedPlatform)
}
fn seek(&mut self, position: Duration) -> Result<(), VideoError> {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
if let Self::Web(backend) = self {
return backend.seek(position);
}
#[cfg(target_os = "android")]
if let Self::Android(backend) = self {
return backend.seek(position);
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
if let Self::Apple(backend) = self {
return backend.seek(position);
}
#[cfg(target_os = "windows")]
if let Self::Windows(backend) = self {
return backend.seek(position);
}
let _ = position;
Err(VideoError::UnsupportedPlatform)
}
fn set_loop(&mut self, looping: bool) -> Result<(), VideoError> {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
if let Self::Web(backend) = self {
return backend.set_loop(looping);
}
#[cfg(target_os = "android")]
if let Self::Android(backend) = self {
return backend.set_loop(looping);
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
if let Self::Apple(backend) = self {
return backend.set_loop(looping);
}
#[cfg(target_os = "windows")]
if let Self::Windows(backend) = self {
return backend.set_loop(looping);
}
let _ = looping;
Err(VideoError::UnsupportedPlatform)
}
fn set_muted(&mut self, muted: bool) -> Result<(), VideoError> {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
if let Self::Web(backend) = self {
return backend.set_muted(muted);
}
#[cfg(target_os = "android")]
if let Self::Android(backend) = self {
return backend.set_muted(muted);
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
if let Self::Apple(backend) = self {
return backend.set_muted(muted);
}
#[cfg(target_os = "windows")]
if let Self::Windows(backend) = self {
return backend.set_muted(muted);
}
let _ = muted;
Err(VideoError::UnsupportedPlatform)
}
fn set_volume(&mut self, volume: f32) -> Result<(), VideoError> {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
if let Self::Web(backend) = self {
return backend.set_volume(volume);
}
#[cfg(target_os = "android")]
if let Self::Android(backend) = self {
return backend.set_volume(volume);
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
if let Self::Apple(backend) = self {
return backend.set_volume(volume);
}
#[cfg(target_os = "windows")]
if let Self::Windows(backend) = self {
return backend.set_volume(volume);
}
let _ = volume;
Err(VideoError::UnsupportedPlatform)
}
fn set_playback_rate(&mut self, rate: f64) -> Result<(), VideoError> {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
if let Self::Web(backend) = self {
return backend.set_playback_rate(rate);
}
#[cfg(target_os = "android")]
if let Self::Android(backend) = self {
return backend.set_playback_rate(rate);
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
if let Self::Apple(backend) = self {
return backend.set_playback_rate(rate);
}
#[cfg(target_os = "windows")]
if let Self::Windows(backend) = self {
return backend.set_playback_rate(rate);
}
let _ = rate;
Err(VideoError::UnsupportedPlatform)
}
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
struct WebVideoBackend {
video: web_sys::HtmlVideoElement,
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
impl WebVideoBackend {
fn open(source: &MediaSource) -> Result<Self, VideoError> {
use wasm_bindgen::JsCast;
let document = web_sys::window()
.and_then(|window| window.document())
.ok_or_else(|| VideoError::Backend("browser document is unavailable".to_string()))?;
let video = document
.create_element("video")
.map_err(|error| VideoError::Backend(format!("failed to create video: {error:?}")))?
.dyn_into::<web_sys::HtmlVideoElement>()
.map_err(|_| VideoError::Backend("created element is not a video".to_string()))?;
video.set_cross_origin(Some("anonymous"));
video.set_preload("auto");
video.set_controls(false);
video.set_attribute("playsinline", "").map_err(|error| {
VideoError::Backend(format!("failed to configure inline video: {error:?}"))
})?;
video
.set_attribute("webkit-playsinline", "")
.map_err(|error| {
VideoError::Backend(format!("failed to configure iOS video: {error:?}"))
})?;
video.set_src(&source.uri());
video.load();
Ok(Self { video })
}
fn close(&mut self) {
let _ = self.video.pause();
self.video.set_src("");
self.video.load();
}
fn update(&mut self) -> Result<BackendSnapshot, VideoError> {
use web_sys::HtmlMediaElement;
let width = self.video.video_width();
let height = self.video.video_height();
let ready = self.video.ready_state() >= HtmlMediaElement::HAVE_CURRENT_DATA;
let duration = self.video.duration();
let duration = duration
.is_finite()
.then(|| Duration::from_secs_f64(duration.max(0.0)));
let position = Duration::from_secs_f64(self.video.current_time().max(0.0));
if width == 0 || height == 0 || !ready {
return Ok(BackendSnapshot {
state: VideoState::Opening,
duration,
position,
dimensions: None,
rgba: None,
external_video: None,
});
}
let state = if self.video.ended() {
VideoState::Ended
} else if self.video.paused() {
if self.video.current_time() == 0.0 {
VideoState::Ready
} else {
VideoState::Paused
}
} else {
VideoState::Playing
};
Ok(BackendSnapshot {
state,
duration,
position,
dimensions: Some((width, height)),
rgba: None,
external_video: Some(self.video.clone()),
})
}
fn play(&mut self) -> Result<(), VideoError> {
self.video.play().map(|_| ()).map_err(|error| {
VideoError::Backend(format!("browser refused to play video: {error:?}"))
})
}
fn pause(&mut self) -> Result<(), VideoError> {
let _ = self.video.pause();
Ok(())
}
fn stop(&mut self) -> Result<(), VideoError> {
let _ = self.video.pause();
self.video.set_current_time(0.0);
Ok(())
}
fn seek(&mut self, position: Duration) -> Result<(), VideoError> {
self.video.set_current_time(position.as_secs_f64());
Ok(())
}
fn set_loop(&mut self, looping: bool) -> Result<(), VideoError> {
self.video.set_loop(looping);
Ok(())
}
fn set_muted(&mut self, muted: bool) -> Result<(), VideoError> {
self.video.set_muted(muted);
Ok(())
}
fn set_volume(&mut self, volume: f32) -> Result<(), VideoError> {
self.video.set_volume(volume as f64);
Ok(())
}
fn set_playback_rate(&mut self, rate: f64) -> Result<(), VideoError> {
self.video.set_playback_rate(rate);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{MediaSource, VideoPlayer, VideoPlaylist, VideoState};
#[test]
fn media_sources_are_explicit() {
assert_eq!(
MediaSource::url("intro.mp4"),
MediaSource::Url("intro.mp4".into())
);
assert_eq!(
MediaSource::asset("cutscene.webm"),
MediaSource::Asset("cutscene.webm".into())
);
}
#[test]
fn player_starts_closed() {
let player = VideoPlayer::new(MediaSource::url("intro.mp4"));
assert_eq!(player.state(), VideoState::Closed);
assert!(player.texture().is_none());
}
#[test]
fn playlist_starts_at_first_source() {
let mut playlist = VideoPlaylist::new([
MediaSource::asset("intro.mp4"),
MediaSource::asset("ending.mp4"),
]);
assert_eq!(playlist.len(), 2);
assert_eq!(playlist.current_index(), Some(0));
assert_eq!(
playlist.current_source(),
Some(&MediaSource::Asset("intro.mp4".into()))
);
assert_eq!(playlist.state(), VideoState::Closed);
playlist.index = 1;
assert_eq!(playlist.next_index(), None);
playlist.set_loop(true);
assert_eq!(playlist.next_index(), Some(0));
}
}