//----------------------------------------------------------------------------- // Copyright 2015-2025 RenderHeads Ltd. All rights reserved. //----------------------------------------------------------------------------- using System.Collections; using System.Collections.Generic; using UnityEngine; namespace RenderHeads.Media.AVProVideo { [System.Serializable] public class MediaPlaylist { [System.Serializable] public class MediaItem { public enum SourceType { AVProVideoPlayer, } [SerializeField] public string name = string.Empty; [SerializeField] public SourceType sourceType = SourceType.AVProVideoPlayer; [SerializeField] public MediaPath mediaPath = new MediaPath(); [SerializeField] public Texture2D texture = null; [SerializeField] public float textureDuration; [SerializeField] public bool loop = false; [SerializeField] public PlaylistMediaPlayer.StartMode startMode = PlaylistMediaPlayer.StartMode.Immediate; [SerializeField] public PlaylistMediaPlayer.ProgressMode progressMode = PlaylistMediaPlayer.ProgressMode.OnFinish; [SerializeField] public float progressTimeSeconds = 0.5f; [SerializeField] public bool isOverrideTransition = false; [SerializeField] public PlaylistMediaPlayer.Transition overrideTransition = PlaylistMediaPlayer.Transition.None; [SerializeField] public float overrideTransitionDuration = 1f; [SerializeField] public Easing.Preset overrideTransitionEasing = Easing.Preset.Linear; } [SerializeField] List _items = new List(8); public List Items { get { return _items; } } public bool HasItemAt(int index) { return index >= 0 && index < _items.Count; } } /// /// This is a BETA component /// [AddComponentMenu("AVPro Video/Playlist Media Player", -80)] [HelpURL("https://www.renderheads.com/products/avpro-video/")] public class PlaylistMediaPlayer : MediaPlayer, ITextureProducer { public enum Transition { None, Fade, Black, White, Transparent, Horiz, Vert, Diag, MirrorH, MirrorV, MirrorD, ScrollV, ScrollH, Circle, Diamond, Blinds, Arrows, SlideH, SlideV, Zoom, RectV, Random, } public enum PlaylistLoopMode { None, Loop, } public enum StartMode { Immediate, //AfterSeconds, Manual, } public enum ProgressMode { OnFinish, BeforeFinish, //AfterTime, Manual, } [SerializeField] Shader _transitionShader = null; [SerializeField] MediaPlayer _playerA = null; [SerializeField] MediaPlayer _playerB = null; [SerializeField] bool _playlistAutoProgress = true; [Tooltip("Close the video on the other MediaPlayer when it is not visible any more. This is useful for freeing up memory and GPU decoding resources.")] [SerializeField] bool _autoCloseVideo = true; [SerializeField] PlaylistLoopMode _playlistLoopMode = PlaylistLoopMode.None; [SerializeField] MediaPlaylist _playlist = new MediaPlaylist(); [Tooltip("Pause the previously playing video. This is useful for systems that will struggle to play 2 videos at once")] [SerializeField] bool _pausePreviousOnTransition = true; [SerializeField] Transition _defaultTransition = Transition.None; [SerializeField] float _defaultTransitionDuration = 1f; [SerializeField] Easing.Preset _defaultTransitionEasing = Easing.Preset.Linear; [SerializeField, Range(0.0f, 1.0f)] float _playlistAudioVolume = 1.0f; [SerializeField] bool _playlistAudioMuted = false; private static readonly LazyShaderProperty PropFromTex = new LazyShaderProperty("_FromTex"); private static readonly LazyShaderProperty PropFade = new LazyShaderProperty("_Fade"); private bool _isPaused = false; private int _playlistIndex = 0; private MediaPlayer _nextPlayer; private Material _material; private Transition _currentTransition = Transition.None; private string _currentTransitionName = "LERP_NONE"; private float _currentTransitionDuration = 1f; private Easing.Preset _currentTransitionEasing = Easing.Preset.Linear; private float _transitionTimer = float.MaxValue; private System.Func _easeFunc; private RenderTexture _rt; private MediaPlaylist.MediaItem _currentItem; private MediaPlaylist.MediaItem _nextItem; public MediaPlayer CurrentPlayer { get { if (NextPlayer == _playerA) { return _playerB; } return _playerA; } } public MediaPlayer NextPlayer { get { return _nextPlayer; } } public MediaPlaylist Playlist { get { return _playlist; } } public int PlaylistIndex { get { return _playlistIndex; } } public MediaPlaylist.MediaItem PlaylistItem { get { return _playlist.HasItemAt(_playlistIndex) ? _playlist.Items[_playlistIndex] : null; } } /// /// The default transition to use if the transition is not overridden in the MediaItem /// public Transition DefaultTransition { get { return _defaultTransition; } set { _defaultTransition = value; } } /// /// The default duration the transition will take (in seconds) if the transition is not overridden in the MediaItem /// public float DefaultTransitionDuration { get { return _defaultTransitionDuration; } set { _defaultTransitionDuration = value; } } /// /// The default easing the transition will use if the transition is not overridden in the MediaItem /// public Easing.Preset DefaultTransitionEasing { get { return _defaultTransitionEasing; } set { _defaultTransitionEasing = value; } } /// /// Closes videos that aren't playing. This will save memory but adds extra overhead /// public bool AutoCloseVideo { get { return _autoCloseVideo; } set { _autoCloseVideo = value; } } /// /// None: Do not loop the playlist when the end is reached.
Loop: Rewind the playlist and play again when the each is reached ///
public PlaylistLoopMode LoopMode { get { return _playlistLoopMode; } set { _playlistLoopMode = value; } } /// /// Enable the playlist to progress to the next item automatically, or wait for manual trigger via scripting /// public bool AutoProgress { get { return _playlistAutoProgress; } set { _playlistAutoProgress = value; } } /// /// Returns the IMediaInfo interface for the MediaPlayer that is playing the current active item in the playlist (returned by CurrentPlayer property). This will change during each transition. /// public override IMediaInfo Info { get { return CurrentPlayer != null ? CurrentPlayer.Info : null; } } /// /// Returns the IMediaControl interface for the MediaPlayer that is playing the current active item in the playlist (returned by CurrentPlayer property). This will change during each transition. /// public override IMediaControl Control { get { return CurrentPlayer != null ? CurrentPlayer.Control : null; } } public override ITextureProducer TextureProducer { get { return this; } } public override float AudioVolume { get { return _playlistAudioVolume; } set { _playlistAudioVolume = Mathf.Clamp01(value); if (!IsTransitioning() && CurrentPlayer != null) { CurrentPlayer.AudioVolume = _playlistAudioVolume; } } } public override bool AudioMuted { get { return _playlistAudioMuted; } set { _playlistAudioMuted = value; if (!IsTransitioning() && CurrentPlayer != null) { CurrentPlayer.AudioMuted = _playlistAudioMuted; } } } public override void Play() { _isPaused = false; if (Control != null) { Control.Play(); } if (IsTransitioning()) { if (!_pausePreviousOnTransition && NextPlayer.Control != null) { NextPlayer.Control.Play(); } } } public override void Pause() { _isPaused = true; if (Control != null) { Control.Pause(); } if (IsTransitioning()) { if (NextPlayer.Control != null) { NextPlayer.Control.Pause(); } } } public bool IsPaused() { return _isPaused; } private WaitForEndOfFrame _waitForEndOfFrame = new WaitForEndOfFrame(); private IEnumerator SwapPlayers() { // Need to wait for rendering to complete before swapping the players yield return _waitForEndOfFrame; // Pause the previously playing video // This is useful for systems that will struggle to play 2 videos at once if (_pausePreviousOnTransition) { CurrentPlayer.Pause(); } // Tell listeners that the playlist item has changed Events.Invoke(this, MediaPlayerEvent.EventType.PlaylistItemChanged, ErrorCode.None); // Start the transition if (_currentTransition != Transition.None) { // Create a new transition texture if required Texture currentTexture = GetCurrentPlayerTexture(); Texture nextTexture = GetNextTexture(); if (currentTexture != null && nextTexture != null) { int maxWidth = Mathf.Max(nextTexture.width, currentTexture.width); int maxHeight = Mathf.Max(nextTexture.height, currentTexture.height); if (_rt != null) { if (_rt.width != maxWidth || _rt.height != maxHeight) { RenderTexture.ReleaseTemporary(_rt); _rt = null; } } if (_rt == null) { _rt = RenderTexture.GetTemporary(maxWidth, maxHeight, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Default, 1); } _material.SetTexture(PropFromTex.Id, currentTexture); _material.SetFloat(PropFade.Id, 0.0f); Graphics.Blit(nextTexture, _rt, _material); _easeFunc = Easing.GetFunction(_currentTransitionEasing); _transitionTimer = 0.0f; } else { // Immediately complete the transition _transitionTimer = float.MaxValue; // Immediately update the audio volume NextPlayer.AudioVolume = this.AudioVolume; CurrentPlayer.AudioVolume = 0f; if (_autoCloseVideo) { CurrentPlayer.MediaPath.Path = string.Empty; CurrentPlayer.CloseMedia(); } } } // Swap the videos if (NextPlayer == _playerA) { _nextPlayer = _playerB; } else { _nextPlayer = _playerA; } // Swap the items _currentItem = _nextItem; _nextItem = null; } private Texture GetCurrentPlayerTexture(int index = 0) { if (CurrentPlayer != null && CurrentPlayer.TextureProducer != null) { return CurrentPlayer.TextureProducer.GetTexture(index); } else { return null; } } private Texture GetNextTexture(int index = 0) { if (_nextPlayer != null && _nextPlayer.TextureProducer != null) { return _nextPlayer.TextureProducer.GetTexture(index); } else { return null; } } private void Awake() { _nextPlayer = _playerA; if (_transitionShader == null) { _transitionShader = Shader.Find("AVProVideo/Internal/Transition"); if (_transitionShader == null) { Debug.LogError("[AVProVideo] Missing transition shader"); } } _material = new Material(_transitionShader); _easeFunc = Easing.GetFunction(_defaultTransitionEasing); } protected override void OnDestroy() { if (_rt != null) { RenderTexture.ReleaseTemporary(_rt); _rt = null; } if (_material != null) { if (Application.isPlaying) { Material.Destroy(_material); } else { Material.DestroyImmediate(_material); } _material = null; } base.OnDestroy(); } private void Start() { if (Application.isPlaying) { if (CurrentPlayer) { CurrentPlayer.Events.AddListener(OnMediaPlayerEvent); if (NextPlayer) { NextPlayer.Events.AddListener(OnMediaPlayerEvent); } } JumpToItem(0); } } public void OnMediaPlayerEvent(MediaPlayer mediaPlayer, MediaPlayerEvent.EventType eventType, ErrorCode errorCode) { if (mediaPlayer == CurrentPlayer) { Events.Invoke(mediaPlayer, eventType, errorCode); } switch (eventType) { case MediaPlayerEvent.EventType.FirstFrameReady: if (mediaPlayer == NextPlayer) { StartCoroutine(SwapPlayers()); Events.Invoke(mediaPlayer, eventType, errorCode); } break; case MediaPlayerEvent.EventType.FinishedPlaying: if (mediaPlayer == CurrentPlayer) { if (_playlistAutoProgress && _currentItem.progressMode == ProgressMode.OnFinish) { NextItem(); } } break; } } public bool PrevItem() { return JumpToItem(_playlistIndex - 1); } public bool NextItem() { bool result = JumpToItem(_playlistIndex + 1); if (!result) { Events.Invoke(this, MediaPlayerEvent.EventType.PlaylistFinished, ErrorCode.None); } return result; } public bool CanJumpToItem(int index) { if (_playlistLoopMode == PlaylistLoopMode.Loop) { if (_playlist.Items.Count > 0) { index %= _playlist.Items.Count; if (index < 0) { index += _playlist.Items.Count; } } } return _playlist.HasItemAt(index); } public bool JumpToItem(int index) { if (_playlistLoopMode == PlaylistLoopMode.Loop) { if (_playlist.Items.Count > 0) { index %= _playlist.Items.Count; if (index < 0) { index += _playlist.Items.Count; } } } if (_playlist.HasItemAt(index)) { _playlistIndex = index; _nextItem = _playlist.Items[_playlistIndex]; OpenVideoFile(_nextItem); return true; } return false; } public void OpenVideoFile(MediaPlaylist.MediaItem mediaItem) { bool isMediaAlreadyLoaded = false; if (NextPlayer.MediaPath == mediaItem.mediaPath) { isMediaAlreadyLoaded = true; } if (!mediaItem.isOverrideTransition) { SetTransition(_defaultTransition, _defaultTransitionDuration, _defaultTransitionEasing); } else { SetTransition(mediaItem.overrideTransition, mediaItem.overrideTransitionDuration, mediaItem.overrideTransitionEasing); } this.Loop = NextPlayer.Loop = mediaItem.loop; NextPlayer.MediaPath = new MediaPath(mediaItem.mediaPath); this.MediaPath = new MediaPath(mediaItem.mediaPath); NextPlayer.AudioMuted = _playlistAudioMuted; NextPlayer.AudioVolume = _playlistAudioVolume; if (_transitionTimer < _currentTransitionDuration && _currentTransition != Transition.None) { NextPlayer.AudioVolume = 0f; } if (isMediaAlreadyLoaded) { NextPlayer.Rewind(false); if (_nextItem.startMode == StartMode.Immediate) { NextPlayer.Play(); } // TODO: We probably want to wait until the new frame arrives before swapping after a Rewind() StartCoroutine(SwapPlayers()); } else { if (string.IsNullOrEmpty(NextPlayer.MediaPath.Path)) { NextPlayer.CloseMedia(); } else { NextPlayer.OpenMedia(NextPlayer.MediaPath.PathType, NextPlayer.MediaPath.Path, _nextItem.startMode == StartMode.Immediate); } } } private bool IsTransitioning() { bool hasTransition = _currentTransition != Transition.None; bool inTransition = _transitionTimer < _currentTransitionDuration; return hasTransition && inTransition; } private void SetTransition(Transition transition, float duration, Easing.Preset easing) { if (transition == Transition.Random) { transition = (Transition)Random.Range(0, (int)Transition.Random); } if (transition != _currentTransition) { // Disable the previous transition if (!string.IsNullOrEmpty(_currentTransitionName)) { _material.DisableKeyword(_currentTransitionName); } // Enable the next transition _currentTransition = transition; _currentTransitionName = GetTransitionName(transition); _material.EnableKeyword(_currentTransitionName); } _currentTransitionDuration = duration; _currentTransitionEasing = easing; } protected override void Update() { if (!Application.isPlaying) { return; } if (!IsPaused()) { if (IsTransitioning()) { _transitionTimer += Time.deltaTime; float t = _easeFunc(Mathf.Clamp01(_transitionTimer / _currentTransitionDuration)); // Fade the audio volume NextPlayer.AudioVolume = (1f - t) * this.AudioVolume; CurrentPlayer.AudioVolume = t * this.AudioVolume; // TODO: support going from mono to stereo // TODO: support videos of different aspect ratios by rendering with scaling to fit // This can be done by blitting twice, once for each eye // If the stereo mode is different for playera/b then both should be set to stereo during the transition // if (CurrentPlayer.m_StereoPacking == StereoPacking.TopBottom).... _material.SetFloat(PropFade.Id, t); _rt.DiscardContents(); Graphics.Blit(GetCurrentPlayerTexture(), _rt, _material); // After the transition is now complete, close/pause the previous video if required bool isTransitioning = IsTransitioning(); if (!isTransitioning) { if (_autoCloseVideo) { if (NextPlayer != null) { NextPlayer.MediaPath.Path = string.Empty; NextPlayer.CloseMedia(); } } else if (!_pausePreviousOnTransition) { if (NextPlayer != null && NextPlayer.Control.IsPlaying()) { NextPlayer.Pause(); } } } } else if (_playlistAutoProgress) { if (_nextItem == null && _currentItem != null && _currentItem.progressMode == ProgressMode.BeforeFinish && Control != null && Control.HasMetaData() && Control.GetCurrentTime() >= (Info.GetDuration() - _currentItem.progressTimeSeconds)) { this.NextItem(); } else if (_currentItem == null) { JumpToItem(_playlistIndex); } } } base.Update(); } #region Implementing ITextureProducer public Texture GetTexture(int index = 0) { bool isTransitioning = IsTransitioning(); return isTransitioning ? _rt : CurrentPlayer.TextureProducer.GetTexture(index); } public int GetTextureCount() { return CurrentPlayer.TextureProducer.GetTextureCount(); } public int GetTextureFrameCount() { return CurrentPlayer.TextureProducer.GetTextureFrameCount(); } public bool SupportsTextureFrameCount() { return CurrentPlayer.TextureProducer.SupportsTextureFrameCount(); } public long GetTextureTimeStamp() { return CurrentPlayer.TextureProducer.GetTextureTimeStamp(); } public float GetTexturePixelAspectRatio() { return CurrentPlayer.TextureProducer.GetTexturePixelAspectRatio(); } public bool RequiresVerticalFlip() { return CurrentPlayer.TextureProducer.RequiresVerticalFlip(); } public Matrix4x4 GetYpCbCrTransform() { return CurrentPlayer.TextureProducer.GetYpCbCrTransform(); } public StereoPacking GetTextureStereoPacking() { return CurrentPlayer.TextureProducer.GetTextureStereoPacking(); } public TransparencyMode GetTextureTransparency() { return CurrentPlayer.TextureProducer.GetTextureTransparency(); } public AlphaPacking GetTextureAlphaPacking() { return CurrentPlayer.TextureProducer.GetTextureAlphaPacking(); } public float[] GetAffineTransform() { return CurrentPlayer.TextureProducer.GetAffineTransform(); } public Matrix4x4 GetTextureMatrix() { return CurrentPlayer.TextureProducer.GetTextureMatrix(); } public RenderTextureFormat GetCompatibleRenderTextureFormat(GetCompatibleRenderTextureFormatOptions options, int plane) { return CurrentPlayer.TextureProducer.GetCompatibleRenderTextureFormat(options, plane); } #endregion Implementing ITextureProducer private static string GetTransitionName(Transition transition) { switch (transition) { case Transition.None: return "LERP_NONE"; case Transition.Fade: return "LERP_FADE"; case Transition.Black: return "LERP_BLACK"; case Transition.White: return "LERP_WHITE"; case Transition.Transparent: return "LERP_TRANSP"; case Transition.Horiz: return "LERP_HORIZ"; case Transition.Vert: return "LERP_VERT"; case Transition.Diag: return "LERP_DIAG"; case Transition.MirrorH: return "LERP_HORIZ_MIRROR"; case Transition.MirrorV: return "LERP_VERT_MIRROR"; case Transition.MirrorD: return "LERP_DIAG_MIRROR"; case Transition.ScrollV: return "LERP_SCROLL_VERT"; case Transition.ScrollH: return "LERP_SCROLL_HORIZ"; case Transition.Circle: return "LERP_CIRCLE"; case Transition.Diamond: return "LERP_DIAMOND"; case Transition.Blinds: return "LERP_BLINDS"; case Transition.Arrows: return "LERP_ARROW"; case Transition.SlideH: return "LERP_SLIDE_HORIZ"; case Transition.SlideV: return "LERP_SLIDE_VERT"; case Transition.Zoom: return "LERP_ZOOM_FADE"; case Transition.RectV: return "LERP_RECTS_VERT"; } return string.Empty; } } }