diff --git a/FlyleafLib/Controls/WPF/FlyleafHost.cs b/FlyleafLib/Controls/WPF/FlyleafHost.cs index 29eef77..ed1b869 100644 --- a/FlyleafLib/Controls/WPF/FlyleafHost.cs +++ b/FlyleafLib/Controls/WPF/FlyleafHost.cs @@ -138,11 +138,11 @@ IsStandAlone [ReadOnly] static bool isDesignMode; static int idGenerator = 1; - static nint NONE_STYLE = (nint) (WindowStyles.WS_MINIMIZEBOX | WindowStyles.WS_CLIPSIBLINGS | WindowStyles.WS_CLIPCHILDREN | WindowStyles.WS_VISIBLE); // WS_MINIMIZEBOX required for swapchain - static Point zeroPoint; - static POINT zeroPOINT; - static Rect zeroRect; - static CornerRadius zeroCornerRadius; + static WindowStyles NONE_STYLE = WindowStyles.WS_MINIMIZEBOX | WindowStyles.WS_CLIPSIBLINGS | WindowStyles.WS_CLIPCHILDREN | WindowStyles.WS_VISIBLE; // WS_MINIMIZEBOX required for swapchain + static Point zeroPoint = new(); + static POINT zeroPOINT = new(); + static Rect zeroRect = new(); + static CornerRadius zeroCornerRadius= new(); double curResizeRatio; bool surfaceClosed, surfaceClosing, overlayClosed; @@ -523,6 +523,14 @@ public CornerRadius CornerRadius } public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register(nameof(CornerRadius), typeof(CornerRadius), _flType, new(new CornerRadius(0), new(OnCornerRadiusChanged))); + + public Brush VideoBackground + { + get => (Brush)GetValue(VideoBackgroundProperty); + set => SetValue(VideoBackgroundProperty, value); + } + public static readonly DependencyProperty VideoBackgroundProperty = + DependencyProperty.Register(nameof(VideoBackground), typeof(Brush), _flType, new(Brushes.Black, new(OnVideoBackgroundChanged))); #endregion #region Events @@ -580,9 +588,9 @@ private static void OnShowInTaskBarChanged(DependencyObject d, DependencyPropert return; if (host.DetachedShowInTaskbar) - SetWindowLong(host.SurfaceHandle, (int)WindowLongFlags.GWL_EXSTYLE, GetWindowLong(host.SurfaceHandle, (int)WindowLongFlags.GWL_EXSTYLE) | (nint)WindowStylesEx.WS_EX_APPWINDOW); + SetWindowLong(host.SurfaceHandle, GetWindowLongEx(host.SurfaceHandle) | WindowStylesEx.WS_EX_APPWINDOW); else - SetWindowLong(host.SurfaceHandle, (int)WindowLongFlags.GWL_EXSTYLE, GetWindowLong(host.SurfaceHandle, (int)WindowLongFlags.GWL_EXSTYLE) & ~(nint)WindowStylesEx.WS_EX_APPWINDOW); + SetWindowLong(host.SurfaceHandle, GetWindowLongEx(host.SurfaceHandle) & ~WindowStylesEx.WS_EX_APPWINDOW); } private static void OnNoOwnerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { @@ -725,31 +733,20 @@ private static void OnCornerRadiusChanged(DependencyObject d, DependencyProperty if (host.Surface == null) return; - if (host.CornerRadius == zeroCornerRadius) - host.Surface.Background = Brushes.Black; - else - { - host.Surface.Background = Brushes.Transparent; - host.SetCornerRadiusBorder(); - } - if (host?.Player == null) return; host.Player.renderer.CornerRadius = (CornerRadius)e.NewValue; } - private void SetCornerRadiusBorder() + private static void OnVideoBackgroundChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - // Required to handle mouse events as the window's background will be transparent - // This does not set the background color we do that with the renderer (which causes some issues eg. when returning from fullscreen to normalscreen) - Surface.Content = new Border() - { - Background = Brushes.Black, // TBR: for alpha channel -> Background == Brushes.Transparent || Background ==null ? new SolidColorBrush(Color.FromArgb(1,0,0,0)) : Background - HorizontalAlignment = HorizontalAlignment.Stretch, - VerticalAlignment = VerticalAlignment.Stretch, - CornerRadius = CornerRadius, - }; + FlyleafHost host = d as FlyleafHost; + if (host.Disposed) + return; + + if (host.Surface != null) + host.Surface.Background = (Brush)e.NewValue; } private static object OnContentChanging(DependencyObject d, object baseValue) { @@ -885,14 +882,14 @@ private void Host_IsVisibleChanged(object sender, DependencyPropertyChangedEvent { // Detach Overlay SetParent(OverlayHandle, IntPtr.Zero); - SetWindowLong(OverlayHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE); + SetWindowLong(OverlayHandle, NONE_STYLE); Overlay.Owner = null; SetWindowPos(OverlayHandle, IntPtr.Zero, 0, 0, (int)Surface.ActualWidth, (int)Surface.ActualHeight, (uint)(SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE)); // Attache Overlay - SetWindowLong(OverlayHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE | (nint)(WindowStyles.WS_CHILD | WindowStyles.WS_MAXIMIZE)); + SetWindowLong(OverlayHandle, NONE_STYLE | WindowStyles.WS_CHILD | WindowStyles.WS_MAXIMIZE); Overlay.Owner = Surface; SetParent(OverlayHandle, SurfaceHandle); @@ -1536,14 +1533,7 @@ public virtual void SetPlayer(Player oldPlayer) } if (Surface != null) - { - if (CornerRadius == zeroCornerRadius) - Surface.Background = new SolidColorBrush(Player.Config.Video.BackgroundColor); - //else // TBR: this border probably not required? only when we don't have a renderer? - //((Border)Surface.Content).Background = new SolidColorBrush(Player.Config.Video.BackgroundColor); - Player.VideoDecoder.CreateSwapChain(SurfaceHandle); - } } public virtual void SetSurface(bool fromSetOverlay = false) { @@ -1551,23 +1541,23 @@ public virtual void SetSurface(bool fromSetOverlay = false) return; // Required for some reason (WindowStyle.None will not be updated with our style) - Surface = new(); - Surface.Name = $"Surface_{UniqueId}"; - Surface.Width = Surface.Height = 1; // Will be set on loaded - Surface.WindowStyle = WindowStyle.None; - Surface.ResizeMode = ResizeMode.NoResize; - Surface.ShowInTaskbar = false; - - // CornerRadius must be set initially to AllowsTransparency! - if (_CornerRadius == zeroCornerRadius) - Surface.Background = Player != null ? new SolidColorBrush(Player.Config.Video.BackgroundColor) : Brushes.Black; - else - { - Surface.AllowsTransparency = true; - Surface.Background = Brushes.Transparent; - SetCornerRadiusBorder(); - } + Surface = new() + { + Name = $"Surface_{UniqueId}", + Width = 1, + Height = 1, + WindowStyle = WindowStyle.None, + ResizeMode = ResizeMode.NoResize, + ShowInTaskbar = false, + Background = VideoBackground + }; + // NOTE: AllowsTransparency will cause performance issues (enable only if really required) + if (Surface.Background is SolidColorBrush scb) + Surface.AllowsTransparency = scb.Color.A < 255; + else + Surface.AllowsTransparency = true; // Non-solid consider true? + // When using ItemsControl with ObservableCollection to fill DataTemplates with FlyleafHost EnsureHandle will call Host_loaded if (_IsAttached) Loaded -= Host_Loaded; SurfaceHandle = new WindowInteropHelper(Surface).EnsureHandle(); @@ -1575,20 +1565,22 @@ public virtual void SetSurface(bool fromSetOverlay = false) if (_IsAttached) { - SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE | (nint)WindowStyles.WS_CHILD); - SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_EXSTYLE, (nint)WindowStylesEx.WS_EX_LAYERED); + SetWindowLong(SurfaceHandle, NONE_STYLE | WindowStyles.WS_CHILD); + SetWindowLong(SurfaceHandle, WindowStylesEx.WS_EX_LAYERED); } else // Detached || StandAlone { - SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE); + SetWindowLong(SurfaceHandle, NONE_STYLE); if (DetachedShowInTaskbar || IsStandAlone) - SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_EXSTYLE, (nint)(WindowStylesEx.WS_EX_APPWINDOW | WindowStylesEx.WS_EX_LAYERED)); + SetWindowLong(SurfaceHandle, WindowStylesEx.WS_EX_APPWINDOW | WindowStylesEx.WS_EX_LAYERED); else - SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_EXSTYLE, (nint)WindowStylesEx.WS_EX_LAYERED); + SetWindowLong(SurfaceHandle, WindowStylesEx.WS_EX_LAYERED); } - if (Player != null) - Player.VideoDecoder.CreateSwapChain(SurfaceHandle); + // AllowsTransparency = true will ignore it (if we have it with other flags it might cause issues) + SetWindowLong(SurfaceHandle, GetWindowLongEx(SurfaceHandle) | WindowStylesEx.WS_EX_NOREDIRECTIONBITMAP); + + Player?.VideoDecoder.CreateSwapChain(SurfaceHandle); Surface.IsVisibleChanged += Surface_IsVisibleChanged; @@ -1667,7 +1659,7 @@ public virtual void SetOverlay() Overlay.ShowInTaskbar = false; Overlay.Owner = Surface; SetParent(OverlayHandle, SurfaceHandle); - SetWindowLong(OverlayHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE | (nint)(WindowStyles.WS_CHILD | WindowStyles.WS_MAXIMIZE)); // TBR: WS_MAXIMIZE required? (possible better for DWM on fullscreen?) + SetWindowLong(OverlayHandle, NONE_STYLE | WindowStyles.WS_CHILD | WindowStyles.WS_MAXIMIZE); // TBR: WS_MAXIMIZE required? (possible better for DWM on fullscreen?) Overlay.KeyUp += Overlay_KeyUp; Overlay.KeyDown += Overlay_KeyDown; @@ -1836,7 +1828,7 @@ public virtual void Attach(bool ignoreRestoreRect = false) Surface.MaxWidth = MaxWidth; Surface.MaxHeight = MaxHeight; - SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE | (nint)WindowStyles.WS_CHILD); + SetWindowLong(SurfaceHandle, NONE_STYLE | WindowStyles.WS_CHILD); Surface.Owner = Owner; SetParent(SurfaceHandle, OwnerHandle); @@ -1913,7 +1905,7 @@ public virtual void Detach() // Detach (Parent=Null, Owner=Null ?, ShowInTaskBar?, TopMost?) SetParent(SurfaceHandle, IntPtr.Zero); - SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE); // TBR (also in Attach/FullScren): Needs to be after SetParent. when detached and trying to close the owner will take two clicks (like mouse capture without release) //SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, GetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE) & ~(nint)WindowStyles.WS_CHILD); + SetWindowLong(SurfaceHandle, NONE_STYLE); // TBR (also in Attach/FullScren): Needs to be after SetParent. when detached and trying to close the owner will take two clicks (like mouse capture without release) //SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, GetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE) & ~(nint)WindowStyles.WS_CHILD); Surface.Owner = DetachedNoOwner ? null : Owner; Surface.Topmost = DetachedTopMost; @@ -1938,7 +1930,7 @@ public void RefreshNormalFullScreen() ResetVisibleRect(); SetParent(SurfaceHandle, IntPtr.Zero); - SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, NONE_STYLE); // TBR (also in Attach/FullScren): Needs to be after SetParent. when detached and trying to close the owner will take two clicks (like mouse capture without release) //SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, GetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE) & ~(nint)WindowStyles.WS_CHILD); + SetWindowLong(SurfaceHandle, NONE_STYLE); // TBR (also in Attach/FullScren): Needs to be after SetParent. when detached and trying to close the owner will take two clicks (like mouse capture without release) //SetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE, GetWindowLong(SurfaceHandle, (int)WindowLongFlags.GWL_STYLE) & ~(nint)WindowStyles.WS_CHILD); Surface.Owner = DetachedNoOwner ? null : Owner; Surface.Topmost = DetachedTopMost; @@ -1948,9 +1940,6 @@ public void RefreshNormalFullScreen() if (Player != null) Player.renderer.CornerRadius = zeroCornerRadius; - if (CornerRadius != zeroCornerRadius) - ((Border)Surface.Content).CornerRadius = zeroCornerRadius; - if (Overlay != null) { Overlay.Hide(); @@ -1987,9 +1976,6 @@ public void RefreshNormalFullScreen() if (Player != null) Player.renderer.CornerRadius = CornerRadius; - if (CornerRadius != zeroCornerRadius) - ((Border)Surface.Content).CornerRadius = CornerRadius; - if (!IsStandAlone) //when play with alpha video and not standalone, we need to set window state to normal last, otherwise it will be lost the background Surface.WindowState = WindowState.Normal; diff --git a/FlyleafLib/Engine/Config.cs b/FlyleafLib/Engine/Config.cs index 5417ab7..8a46671 100644 --- a/FlyleafLib/Engine/Config.cs +++ b/FlyleafLib/Engine/Config.cs @@ -454,101 +454,100 @@ public long BufferDuration { /// /// Maximuim allowed packets for buffering (as an extra check along with BufferDuration) /// - public long BufferPackets { get; set; } + public long BufferPackets { get; set; } /// /// Maximuim allowed audio packets (when reached it will drop the extra packets and will fire the AudioLimit event) /// - public long MaxAudioPackets { get; set; } + public long MaxAudioPackets { get; set; } /// /// Maximum allowed errors before stopping /// - public int MaxErrors { get; set; } = 30; + public int MaxErrors { get; set; } = 30; /// /// Custom IO Stream buffer size (in bytes) for the AVIO Context /// - public int IOStreamBufferSize - { get; set; } = 0x200000; + public int IOStreamBufferSize { get; set; } = 0x200000; /// /// avformat_close_input timeout (ticks) for protocols that support interrupts /// - public long CloseTimeout { get => closeTimeout; set { closeTimeout = value; closeTimeoutMs = value / 10000; } } + public long CloseTimeout { get => closeTimeout; set { closeTimeout = value; closeTimeoutMs = value / 10000; } } private long closeTimeout = 1 * 1000 * 10000; internal long closeTimeoutMs = 1 * 1000; /// /// avformat_open_input + avformat_find_stream_info timeout (ticks) for protocols that support interrupts (should be related to probesize/analyzeduration) /// - public long OpenTimeout { get => openTimeout; set { openTimeout = value; openTimeoutMs = value / 10000; } } + public long OpenTimeout { get => openTimeout; set { openTimeout = value; openTimeoutMs = value / 10000; } } private long openTimeout = 5 * 60 * (long)1000 * 10000; internal long openTimeoutMs = 5 * 60 * 1000; /// /// av_read_frame timeout (ticks) for protocols that support interrupts /// - public long ReadTimeout { get => readTimeout; set { readTimeout = value; readTimeoutMs = value / 10000; } } + public long ReadTimeout { get => readTimeout; set { readTimeout = value; readTimeoutMs = value / 10000; } } private long readTimeout = 10 * 1000 * 10000; internal long readTimeoutMs = 10 * 1000; /// /// av_read_frame timeout (ticks) for protocols that support interrupts (for Live streams) /// - public long ReadLiveTimeout { get => readLiveTimeout; set { readLiveTimeout = value; readLiveTimeoutMs = value / 10000; } } + public long ReadLiveTimeout { get => readLiveTimeout; set { readLiveTimeout = value; readLiveTimeoutMs = value / 10000; } } private long readLiveTimeout = 20 * 1000 * 10000; internal long readLiveTimeoutMs = 20 * 1000; /// /// av_seek_frame timeout (ticks) for protocols that support interrupts /// - public long SeekTimeout { get => seekTimeout; set { seekTimeout = value; seekTimeoutMs = value / 10000; } } + public long SeekTimeout { get => seekTimeout; set { seekTimeout = value; seekTimeoutMs = value / 10000; } } private long seekTimeout = 8 * 1000 * 10000; internal long seekTimeoutMs = 8 * 1000; /// /// Forces Input Format /// - public string ForceFormat { get; set; } + public string ForceFormat { get; set; } /// /// Forces FPS for NoTimestamp formats (such as h264/hevc) /// - public double ForceFPS { get; set; } + public double ForceFPS { get; set; } /// /// FFmpeg's format flags for demuxer (see https://ffmpeg.org/doxygen/trunk/avformat_8h.html) /// eg. FormatFlags |= 0x40; // For AVFMT_FLAG_NOBUFFER /// - public DemuxerFlags FormatFlags { get; set; } = DemuxerFlags.DiscardCorrupt;// FFmpeg.AutoGen.ffmpeg.AVFMT_FLAG_DISCARD_CORRUPT; + public DemuxerFlags FormatFlags { get; set; } = DemuxerFlags.DiscardCorrupt;// FFmpeg.AutoGen.ffmpeg.AVFMT_FLAG_DISCARD_CORRUPT; /// /// Certain muxers and demuxers do nesting (they open one or more additional internal format contexts). This will pass the FormatOpt and HTTPQuery params to the underlying contexts) /// public bool FormatOptToUnderlying - { get; set; } + { get; set; } /// /// Passes original's Url HTTP Query String parameters to underlying /// public bool DefaultHTTPQueryToUnderlying - { get; set; } = true; + { get; set; } = true; /// /// HTTP Query String parameters to pass to underlying /// public Dictionary ExtraHTTPQueryParamsToUnderlying - { get; set; } = []; + { get; set; } = []; /// /// FFmpeg's format options for demuxer /// public Dictionary - FormatOpt { get; set; } = DefaultVideoFormatOpt(); + FormatOpt { get; set; } = DefaultVideoFormatOpt(); public Dictionary - AudioFormatOpt { get; set; } = DefaultVideoFormatOpt(); + AudioFormatOpt { get; set; } = DefaultVideoFormatOpt(); public Dictionary SubtitlesFormatOpt { get; set; } = DefaultVideoFormatOpt(); @@ -603,54 +602,77 @@ public DecoderConfig Clone() /// /// Threads that will be used from the decoder /// - public int VideoThreads { get; set; } = Environment.ProcessorCount; + public int VideoThreads { get; set; } = Environment.ProcessorCount; /// /// Maximum video frames to be decoded and processed for rendering /// - public int MaxVideoFrames { get => _MaxVideoFrames; set { if (Set(ref _MaxVideoFrames, value)) { player?.RefreshMaxVideoFrames(); } } } + public int MaxVideoFrames { get => _MaxVideoFrames; set { if (Set(ref _MaxVideoFrames, value)) { player?.RefreshMaxVideoFrames(); } } } int _MaxVideoFrames = 4; internal void SetMaxVideoFrames(int maxVideoFrames) { _MaxVideoFrames = maxVideoFrames; RaiseUI(nameof(MaxVideoFrames)); } // can be updated by video decoder if fails to allocate them /// /// Maximum audio frames to be decoded and processed for playback /// - public int MaxAudioFrames { get; set; } = 10; + public int MaxAudioFrames { get; set; } = 10; /// /// Maximum subtitle frames to be decoded /// - public int MaxSubsFrames { get; set; } = 1; + public int MaxSubsFrames { get; set; } = 1; /// /// Maximum data frames to be decoded /// - public int MaxDataFrames { get; set; } = 100; + public int MaxDataFrames { get; set; } = 100; /// /// Maximum allowed errors before stopping /// - public int MaxErrors { get; set; } = 200; + public int MaxErrors { get; set; } = 200; /// /// Allows video accceleration even in codec's profile mismatch /// - public bool AllowProfileMismatch - { get => _AllowProfileMismatch; set => SetUI(ref _AllowProfileMismatch, value); } + public bool AllowProfileMismatch{ get => _AllowProfileMismatch; set => SetUI(ref _AllowProfileMismatch, value); } bool _AllowProfileMismatch = true; /// /// Allows corrupted frames (Parses AV_CODEC_FLAG_OUTPUT_CORRUPT to AVCodecContext) /// - public bool ShowCorrupted { get => _ShowCorrupted; set => SetUI(ref _ShowCorrupted, value); } + public bool ShowCorrupted { get => _ShowCorrupted; set => SetUI(ref _ShowCorrupted, value); } bool _ShowCorrupted; /// - /// Forces low delay (Parses AV_CODEC_FLAG_LOW_DELAY to AVCodecContext) (auto-enabled with MaxLatency) + /// Ensures that after a key packet receives a also key frame (can create artifacts on some broken formats when disabled and seek issues when enabled) /// - public bool LowDelay { get => _LowDelay; set => SetUI(ref _LowDelay, value); } + public bool KeyFrameValidation { get => _KeyFrameValidation; set => SetUI(ref _KeyFrameValidation, value); } + internal bool _KeyFrameValidation; + + /// + /// Forces low delay (Parses AV_CODEC_FLAG2_FAST or AV_CODEC_FLAG_LOW_DELAY -based on DropFrames- to AVCodecContext) (auto-enabled with MaxLatency) + /// + public bool LowDelay { get => _LowDelay; set => SetUI(ref _LowDelay, value); } bool _LowDelay; + /// + /// Sets AVCodecContext skip_frame to None or Default (affects also LowDelay) + /// + public bool AllowDropFrames { get => _AllowDropFrames; set => SetUI(ref _AllowDropFrames, value); } + bool _AllowDropFrames; + + public string AudioCodec { get => _AudioCodec; set => SetUI(ref _AudioCodec, value); } + string _AudioCodec; + + public string VideoCodec { get => _VideoCodec; set => SetUI(ref _VideoCodec, value); } + string _VideoCodec; + + public string SubtitlesCodec { get => _SubtitlesCodec; set => SetUI(ref _SubtitlesCodec, value); } + string _SubtitlesCodec; + + public string GetCodecPtr(MediaType type) + => type == MediaType.Video ? _VideoCodec : type == MediaType.Audio ? _AudioCodec : _SubtitlesCodec; + public Dictionary AudioCodecOpt { get; set; } = []; public Dictionary @@ -665,8 +687,8 @@ public class VideoConfig : NotifyPropertyChanged { public VideoConfig() { - BindingOperations.EnableCollectionSynchronization(Filters, lockFilters); - BindingOperations.EnableCollectionSynchronization(D3Filters, lockD3Filters); + BindingOperations.EnableCollectionSynchronization(Filters, lockFilters); + BindingOperations.EnableCollectionSynchronization(D3Filters,lockD3Filters); } public VideoConfig Clone() @@ -1167,7 +1189,7 @@ public string SearchLocalPaths /// Allowed input types to be searched locally for subtitles (empty list allows all types) /// public List SearchLocalOnInputType - { get; set; } = [ InputType.File, InputType.UNC, InputType.Torrent ]; + { get; set; } = [InputType.File, InputType.UNC, InputType.Torrent]; /// /// Whether to use online search plugins (see also ) @@ -1179,7 +1201,7 @@ public List SearchLocalOnInputType /// Allowed input types to be searched online for subtitles (empty list allows all types) /// public List SearchOnlineOnInputType - { get; set; } = [ InputType.File, InputType.UNC, InputType.Torrent ]; + { get; set; } = [InputType.File, InputType.UNC, InputType.Torrent]; /// /// Subtitles parser (can be used for custom parsing) diff --git a/FlyleafLib/FlyleafLib.csproj b/FlyleafLib/FlyleafLib.csproj index 6b3209b..b6c371a 100644 --- a/FlyleafLib/FlyleafLib.csproj +++ b/FlyleafLib/FlyleafLib.csproj @@ -7,7 +7,7 @@ true Media Player .NET Library for WinUI 3/WPF/WinForms (based on FFmpeg/DirectX) - 3.9.2 + 3.9.3 SuRGeoNix SuRGeoNix © 2025 GPL-3.0-or-later @@ -42,12 +42,12 @@ - - - - - - + + + + + + diff --git a/FlyleafLib/MediaFramework/MediaContext/DecoderContext.cs b/FlyleafLib/MediaFramework/MediaContext/DecoderContext.cs index 600750a..ceaaf1e 100644 --- a/FlyleafLib/MediaFramework/MediaContext/DecoderContext.cs +++ b/FlyleafLib/MediaFramework/MediaContext/DecoderContext.cs @@ -673,7 +673,7 @@ public long GetVideoFrame(long timestamp = -1) if (packet->flags.HasFlag(PktFlags.Key) || packet->pts == VideoDecoder.startPts) { VideoDecoder.keyPacketRequired = false; - VideoDecoder.keyFrameRequired = true; + VideoDecoder.keyFrameRequired = Config.Decoder._KeyFrameValidation; } else { diff --git a/FlyleafLib/MediaFramework/MediaDecoder/AudioDecoder.cs b/FlyleafLib/MediaFramework/MediaDecoder/AudioDecoder.cs index 27e986d..54651e3 100644 --- a/FlyleafLib/MediaFramework/MediaDecoder/AudioDecoder.cs +++ b/FlyleafLib/MediaFramework/MediaDecoder/AudioDecoder.cs @@ -39,7 +39,6 @@ public readonly public ConcurrentQueue Frames { get; protected set; } = new(); - internal Action CBufAlloc; // Informs Audio player to clear buffer pointers to avoid access violation static readonly int cBufTimesSize = 4; // Extra for draining / filters (speed) int cBufTimesCur = 1; byte[] cBuf; @@ -463,7 +462,6 @@ private void AllocateCircularBuffer(int samples) cBufHistory.Dequeue(); } - CBufAlloc?.Invoke(); cBuf = new byte[size]; cBufPos = 0; cBufSamples = samples; diff --git a/FlyleafLib/MediaFramework/MediaDecoder/DecoderBase.cs b/FlyleafLib/MediaFramework/MediaDecoder/DecoderBase.cs index 862be81..f54cc5c 100644 --- a/FlyleafLib/MediaFramework/MediaDecoder/DecoderBase.cs +++ b/FlyleafLib/MediaFramework/MediaDecoder/DecoderBase.cs @@ -61,6 +61,7 @@ public string Open(StreamBase stream) protected string Open2(StreamBase stream, StreamBase prevStream, bool openStream = true) { string error = null; + AVCodec* codec; try { @@ -76,10 +77,17 @@ protected string Open2(StreamBase stream, StreamBase prevStream, bool openStream if (stream is not DataStream) // if we don't open/use a data codec context why not just push the Data Frames directly from the Demuxer? no need to have DataDecoder* { - // avcodec_find_decoder will use libdav1d which does not support hardware decoding (software fallback with openStream = false from av1 to default:libdav1d) [#340] - var codec = stream.CodecID == AVCodecID.Av1 && openStream && Config.Video.VideoAcceleration ? avcodec_find_decoder_by_name("av1") : avcodec_find_decoder(stream.CodecID); + var codecStr = Config.Decoder.GetCodecPtr(stream.Type); + if (string.IsNullOrEmpty(codecStr)) + // avcodec_find_decoder will use libdav1d which does not support hardware decoding (software fallback with openStream = false from av1 to default:libdav1d) [#340] + codec = stream.CodecID == AVCodecID.Av1 && openStream && Config.Video.VideoAcceleration ? avcodec_find_decoder_by_name("av1") : avcodec_find_decoder(stream.CodecID); + else + codec = avcodec_find_decoder_by_name(codecStr); + if (codec == null) - return error = $"[{Type} avcodec_find_decoder] No suitable codec found"; + return error = $"[{Type} avcodec_find_decoder] No suitable codec found "; + + if (CanDebug) Log.Debug($"[{Type}] Using {avcodec_get_name(codec->id)} codec"); codecCtx = avcodec_alloc_context3(codec); // Pass codec to use default settings if (codecCtx == null) @@ -96,7 +104,17 @@ protected string Open2(StreamBase stream, StreamBase prevStream, bool openStream codecCtx->flags |= CodecFlags.OutputCorrupt; if (Config.Decoder.LowDelay) - codecCtx->flags |= CodecFlags.LowDelay; + { + if (Config.Decoder.AllowDropFrames) + codecCtx->flags |= CodecFlags.LowDelay; + else + { + codecCtx->skip_frame = AVDiscard.None; + codecCtx->flags2 |= CodecFlags2.Fast; + } + } + else if (!Config.Decoder.AllowDropFrames) + codecCtx->skip_frame = AVDiscard.None; try { ret = Setup(codec); } catch(Exception e) { return error = $"[{Type} Setup] {e.Message}"; } if (ret < 0) @@ -105,7 +123,7 @@ protected string Open2(StreamBase stream, StreamBase prevStream, bool openStream var codecOpts = Config.Decoder.GetCodecOptPtr(stream.Type); AVDictionary* avopt = null; foreach(var optKV in codecOpts) - av_dict_set(&avopt, optKV.Key, optKV.Value, 0); + _ = av_dict_set(&avopt, optKV.Key, optKV.Value, 0); ret = avcodec_open2(codecCtx, null, avopt == null ? null : &avopt); diff --git a/FlyleafLib/MediaFramework/MediaDecoder/VideoDecoder.cs b/FlyleafLib/MediaFramework/MediaDecoder/VideoDecoder.cs index 11f7673..b787557 100644 --- a/FlyleafLib/MediaFramework/MediaDecoder/VideoDecoder.cs +++ b/FlyleafLib/MediaFramework/MediaDecoder/VideoDecoder.cs @@ -9,6 +9,7 @@ using FlyleafLib.MediaFramework.MediaFrame; using FlyleafLib.MediaFramework.MediaRenderer; using FlyleafLib.MediaFramework.MediaRemuxer; +using FlyleafLib.MediaFramework.MediaDemuxer; namespace FlyleafLib.MediaFramework.MediaDecoder; @@ -45,6 +46,9 @@ public ConcurrentQueue bool checkExtraFrames; // DecodeFrameNext int curFrameWidth, curFrameHeight; // To catch 'codec changed' + // Hot paths / Same instance + PacketQueue vPackets; + // Reverse Playback ConcurrentStack> curReverseVideoStack = []; @@ -283,7 +287,8 @@ protected override int Setup(AVCodec* codec) // Ensures we have a renderer (no swap chain is required) CreateRenderer(); - VideoAccelerated = false; + vPackets = demuxer.VideoPackets; + VideoAccelerated= false; if (!swFallback && !Config.Video.SwsForce && Config.Video.VideoAcceleration && Renderer.Device.FeatureLevel >= Vortice.Direct3D.FeatureLevel.Level_10_0) { @@ -400,14 +405,14 @@ protected override void RunInternal() } // While Packets Queue Empty (Drain | Quit if Demuxer stopped | Wait until we get packets) - if (demuxer.VideoPackets.Count == 0) + if (vPackets.IsEmpty) { CriticalArea = true; lock (lockStatus) if (Status == Status.Running) Status = Status.QueueEmpty; - while (demuxer.VideoPackets.Count == 0 && Status == Status.QueueEmpty) + while (vPackets.IsEmpty && Status == Status.QueueEmpty) { if (demuxer.Status == Status.Ended) { @@ -419,7 +424,7 @@ protected override void RunInternal() var drainPacket = av_packet_alloc(); drainPacket->data = null; drainPacket->size = 0; - demuxer.VideoPackets.Enqueue(drainPacket); + vPackets.Enqueue(drainPacket); } break; @@ -467,7 +472,7 @@ protected override void RunInternal() if (Status == Status.Stopped) continue; - packet = demuxer.VideoPackets.Dequeue(); + packet = vPackets.Dequeue(); if (packet == null) continue; @@ -488,7 +493,7 @@ protected override void RunInternal() { if (packet->flags.HasFlag(PktFlags.Key) || packet->pts == startPts) { - keyFrameRequired = true; + keyFrameRequired = Config.Decoder._KeyFrameValidation; keyPacketRequired = false; } else @@ -515,7 +520,7 @@ protected override void RunInternal() if (ret == AVERROR_EOF) { - if (demuxer.VideoPackets.Count > 0) { avcodec_flush_buffers(codecCtx); continue; } // TBR: Happens on HLS while switching video streams + if (!vPackets.IsEmpty) { avcodec_flush_buffers(codecCtx); continue; } // TBR: Happens on HLS while switching video streams Status = Status.Ended; break; } @@ -542,7 +547,7 @@ protected override void RunInternal() if (ret == AVERROR_EOF) { - if (demuxer.VideoPackets.Count > 0) { avcodec_flush_buffers(codecCtx); break; } // TBR: Happens on HLS while switching video streams + if (!vPackets.IsEmpty) { avcodec_flush_buffers(codecCtx); break; } // TBR: Happens on HLS while switching video streams Status = Status.Ended; break; } @@ -1047,7 +1052,7 @@ public int DecodeFrameNext() if (DecodeFrameNextInternal() == 0) return 0; - if (Demuxer.Status == Status.Ended && demuxer.VideoPackets.Count == 0 && Frames.IsEmpty) + if (Demuxer.Status == Status.Ended && vPackets.IsEmpty && Frames.IsEmpty) { Stop(); Status = Status.Ended; @@ -1081,7 +1086,7 @@ public int DecodeFrameNext() if (demuxer.packet->flags.HasFlag(PktFlags.Key) || demuxer.packet->pts == startPts) { keyPacketRequired = false; - keyFrameRequired = true; + keyFrameRequired = Config.Decoder._KeyFrameValidation; } else { diff --git a/FlyleafLib/MediaFramework/MediaDemuxer/Demuxer.cs b/FlyleafLib/MediaFramework/MediaDemuxer/Demuxer.cs index 8b31f09..f9a199e 100644 --- a/FlyleafLib/MediaFramework/MediaDemuxer/Demuxer.cs +++ b/FlyleafLib/MediaFramework/MediaDemuxer/Demuxer.cs @@ -1033,7 +1033,7 @@ public int SeekInQueue(long ticks, bool forward = false) if (ticks + (VideoStream != null && forward ? (10000 * 10000) : 0) > CurTime + startTime && ticks < CurTime + startTime + BufferedDuration) { bool found = false; - while (VideoPackets.Count > 0) + while (!VideoPackets.IsEmpty) { var packet = VideoPackets.Peek(); if (packet->pts != AV_NOPTS_VALUE && ticks < packet->pts * VideoStream.Timebase && (packet->flags & PktFlags.Key) != 0) @@ -1051,7 +1051,7 @@ public int SeekInQueue(long ticks, bool forward = false) av_packet_free(&packet); } - while (AudioPackets.Count > 0) + while (!AudioPackets.IsEmpty) { var packet = AudioPackets.Peek(); if (packet->pts != AV_NOPTS_VALUE && (packet->pts + packet->duration) * AudioStream.Timebase >= ticks) @@ -1068,7 +1068,7 @@ public int SeekInQueue(long ticks, bool forward = false) for (int i = 0; i < subNum; i++) { - while (SubtitlesPackets[i].Count > 0) + while (!SubtitlesPackets[i].IsEmpty) { var packet = SubtitlesPackets[i].Peek(); if (packet->pts != AV_NOPTS_VALUE && ticks < (packet->pts + packet->duration) * SubtitlesStreams[i].Timebase) @@ -1086,7 +1086,7 @@ public int SeekInQueue(long ticks, bool forward = false) } } - while (DataPackets.Count > 0) + while (!DataPackets.IsEmpty) { var packet = DataPackets.Peek(); if (packet->pts != AV_NOPTS_VALUE && ticks < (packet->pts + packet->duration) * DataStream.Timebase) @@ -1890,7 +1890,7 @@ private string GetValidExtension() /// 0 on success public int GetNextVideoPacket() { - if (VideoPackets.Count > 0) + if (!VideoPackets.IsEmpty) { packet = VideoPackets.Dequeue(); return 0; @@ -1944,50 +1944,54 @@ public int GetNextPacket(int streamIndex = -1) #endregion } -public unsafe class PacketQueue +public unsafe class PacketQueue : Queue { // TODO: DTS might not be available without avformat_find_stream_info (should changed based on packet->duration and fallback should be removed) readonly Demuxer demuxer; - readonly ConcurrentQueue packets = []; public long frameDuration = 30 * 1000 * 10000; // in case of negative buffer duration calculate it based on packets count / FPS public long Bytes { get; private set; } public long BufferedDuration { get; private set; } public long CurTime { get; private set; } - public int Count => packets.Count; - public bool IsEmpty => packets.IsEmpty; public long FirstTimestamp { get; private set; } = AV_NOPTS_VALUE; public long LastTimestamp { get; private set; } = AV_NOPTS_VALUE; + public bool IsEmpty => Count == 0; - public PacketQueue(Demuxer demuxer) + public PacketQueue(Demuxer demuxer) : base() => this.demuxer = demuxer; - public void Clear() + #if DEBUG + // Ensures we don't access base queue directly + public new void Enqueue (nint _) => throw new NotImplementedException("Use AVPacket*"); + public new bool TryDequeue (out nint _)=> throw new NotImplementedException("Use AVPacket*"); + public new bool TryPeek (out nint _)=> throw new NotImplementedException("Use AVPacket*"); + #endif + + public new void Clear() { - lock(packets) + lock(this) { - while (!packets.IsEmpty) + while (base.TryDequeue(out nint packetPtr)) { - packets.TryDequeue(out nint packetPtr); - if (packetPtr == 0) continue; + if (packetPtr == 0) continue; // TBR: can be disposed? AVPacket* packet = (AVPacket*)packetPtr; av_packet_free(&packet); } - FirstTimestamp = AV_NOPTS_VALUE; - LastTimestamp = AV_NOPTS_VALUE; - Bytes = 0; - BufferedDuration = 0; - CurTime = 0; + FirstTimestamp = AV_NOPTS_VALUE; + LastTimestamp = AV_NOPTS_VALUE; + Bytes = 0; + BufferedDuration= 0; + CurTime = 0; } } public void Enqueue(AVPacket* packet) { - lock (packets) + lock (this) { - packets.Enqueue((nint)packet); + base.Enqueue((nint)packet); if (packet->dts != AV_NOPTS_VALUE || packet->pts != AV_NOPTS_VALUE) { @@ -2004,20 +2008,20 @@ public void Enqueue(AVPacket* packet) { BufferedDuration = LastTimestamp - FirstTimestamp; if (BufferedDuration < 0) - BufferedDuration = packets.Count * frameDuration; + BufferedDuration = Count * frameDuration; } } else - BufferedDuration = packets.Count * frameDuration; + BufferedDuration = Count * frameDuration; Bytes += packet->size; } } - public AVPacket* Dequeue() + public new AVPacket* Dequeue() { - lock(packets) - if (packets.TryDequeue(out nint packetPtr)) + lock(this) + if (base.TryDequeue(out nint packetPtr)) { AVPacket* packet = (AVPacket*)packetPtr; @@ -2030,7 +2034,7 @@ public void Enqueue(AVPacket* packet) UpdateCurTime(); } else - BufferedDuration = packets.Count * frameDuration; + BufferedDuration = Count * frameDuration; return (AVPacket*)packetPtr; } @@ -2038,8 +2042,8 @@ public void Enqueue(AVPacket* packet) return null; } - public AVPacket* Peek() - => packets.TryPeek(out nint packetPtr) + public new AVPacket* Peek() // Should use lock but we use it only during SeekInQueue (decoders paused) + => base.TryPeek(out nint packetPtr) ? (AVPacket*)packetPtr : (AVPacket*)null; @@ -2068,6 +2072,6 @@ internal void UpdateCurTime() BufferedDuration = LastTimestamp - FirstTimestamp; if (BufferedDuration < 0) - BufferedDuration = packets.Count * frameDuration; + BufferedDuration = Count * frameDuration; } } diff --git a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Device.cs b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Device.cs index 3ad327f..9380551 100644 --- a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Device.cs +++ b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Device.cs @@ -212,7 +212,7 @@ public void Initialize(bool swapChain = true) // subs ShaderBGRA = ShaderCompiler.CompilePS(Device, "bgra", @"color = float4(Texture1.Sample(Sampler, input.Texture).rgba);", null); - // Blend State (currently used -mainly- for RGBA images and OverlayTexture) + // Blend State (currently used for OverlayTexture) blendStateAlpha = Device.CreateBlendState(blendDesc); // Rasterizer (Will change CullMode to None for H-V Flip) diff --git a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.PixelShader.cs b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.PixelShader.cs index 31ab3b6..d746a78 100644 --- a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.PixelShader.cs +++ b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.PixelShader.cs @@ -284,7 +284,6 @@ internal bool ConfigPlanes(AVFrame* frame = null) float4 c1 = Texture1.Sample(Sampler, float2(pos1, input.Texture.y)); float4 c2 = Texture1.Sample(Sampler, float2(pos2, input.Texture.y)); - "; if (VideoStream.PixelFormat == AVPixelFormat.Yuyv422 || VideoStream.PixelFormat == AVPixelFormat.Y210le) @@ -451,10 +450,16 @@ internal bool ConfigPlanes(AVFrame* frame = null) for (int i = 0; i < VideoStream.PixelComps.Length; i++) offsets += pixelOffsets[(int) (VideoStream.PixelComps[i].offset / Math.Ceiling(VideoStream.PixelComp0Depth / 8.0))]; + // TBR: [RGB0]32 has no alpha remove it + if (VideoStream.PixelFormatStr[0] == '0') + offsets = offsets[1..]; + else if (VideoStream.PixelFormatStr[^1] == '0') + offsets = offsets[..^1]; + curPSUniqueId += offsets; string shader; - if (VideoStream.PixelComps.Length > 3) + if (VideoStream.PixelComps.Length > 3 && offsets.Length > 3) shader = @$" color = Texture1.Sample(Sampler, input.Texture).{offsets}; "; @@ -656,7 +661,6 @@ internal bool ConfigPlanes(AVFrame* frame = null) "); } - context.OMSetBlendState(VideoStream.PixelFormatDesc->flags.HasFlag(PixFmtFlags.Alpha) ? blendStateAlpha : null); Log.Debug($"Prepared planes for {VideoStream.PixelFormatStr} with {videoProcessor} [{curPSCase}]"); return true; @@ -735,12 +739,11 @@ internal VideoFrame FillPlanes(AVFrame* frame) bool vflip = false; for (int i = 0; i < VideoStream.PixelPlanes; i++) { - if (frame->linesize[i] < 0) // Negative linesize for vertical flipping + if (frame->linesize[i] < 0) // Negative linesize needs vertical flipping | [Bottom -> Top] data[i] points to last row and we need to move at first (height - 1) rows { vflip = true; subData[0].RowPitch = (uint)(-1 * frame->linesize[i]); - subData[0].DataPointer = frame->data[i]; - subData[0].DataPointer -= (nint)((subData[0].RowPitch * (VideoStream.Height - 1))); // TBR: Heigh is wrong here (needs texture's height?) + subData[0].DataPointer = frame->data[i] + (frame->linesize[i] * (frame->height - 1)); } else { diff --git a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Present.cs b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Present.cs index ebb2565..67c2080 100644 --- a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Present.cs +++ b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Present.cs @@ -109,7 +109,7 @@ void RenderIdle() } internal void RenderPlayStart() { isPlayerPresenting = true; while(isIdlePresenting) Thread.Sleep(1); } // Stop RenderIdle - internal void RenderPlayStop() { isPlayerPresenting = false; RenderRequest(); } // Check if last timestamp?* to start idle (we don't update it currently) + internal void RenderPlayStop() { isPlayerPresenting = false; RenderRequest(); } // TBR: Check if last timestamp?* to start idle internal bool RenderPlay(VideoFrame frame, bool secondField) { try @@ -128,8 +128,7 @@ internal bool RenderPlay(VideoFrame frame, bool secondField) LastFrame = frame; } - // Don't return false when !canRenderPresent as it will cause restarts (consider as valid) - return true; + return true; // Don't return false when !canRenderPresent as it will cause restarts (consider as valid) } } catch (SharpGenException e) @@ -156,7 +155,7 @@ internal bool PresentPlay() try { if (canRenderPresent) - swapChain?.Present(Config.Video.VSync, PresentFlags.None).CheckError(); + swapChain.Present(Config.Video.VSync, PresentFlags.None).CheckError(); return true; } @@ -244,7 +243,7 @@ void RenderFrame(VideoFrame frame, bool secondField) // From Play or Idle (No lo // restore context context.PSSetShader(ShaderPS); - context.OMSetBlendState(VideoStream.PixelFormatDesc->flags.HasFlag(PixFmtFlags.Alpha) ? blendStateAlpha : null); + context.OMSetBlendState(null); context.RSSetViewport(GetViewport); } } @@ -268,7 +267,24 @@ public FrameStatistics GetFrameStatistics() } } - public void ClearScreen(bool force = false) { if (Config.Video.ClearScreen || force) RenderRequest(null, true); } + public void ClearScreen(bool force = false) + { + if (force) + { + lock (lockDevice) + { + if (SCDisposed) + return; + + ClearOverlayTexture(); + context.OMSetRenderTargets(backBufferRtv); + context.ClearRenderTargetView(backBufferRtv, Config.Video._BackgroundColor); + swapChain.Present(1, 0); + } + } + else if (Config.Video.ClearScreen) + RenderRequest(null, true); + } public void ClearOverlayTexture() { if (overlayTexture == null) diff --git a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.SwapChain.cs b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.SwapChain.cs index fdf65ce..f4c79ab 100644 --- a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.SwapChain.cs +++ b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.SwapChain.cs @@ -27,7 +27,7 @@ unsafe public partial class Renderer const uint WM_SIZE = 0x0005; const uint WM_DISPLAYCHANGE = 0x007E; const uint WM_NCDESTROY = 0x0082; - const int WS_EX_NOREDIRECTIONBITMAP = 0x00200000; + SubclassWndProc wndProcDelegate; IntPtr wndProcDelegatePtr; @@ -68,9 +68,6 @@ internal void InitializeSwapChain(nint handle) ControlWidth = rect.Right - rect.Left; ControlHeight = rect.Bottom - rect.Top; - // WS_EX_NOREDIRECTIONBITMAP: Prevent DWM from creating a redirection surface (offscreen bitmap) - SetWindowLong(handle, (int)WindowLongFlags.GWL_EXSTYLE, GetWindowLong(handle, (int)WindowLongFlags.GWL_EXSTYLE).ToInt32() | WS_EX_NOREDIRECTIONBITMAP); - try { if (CanInfo) Log.Info($"Initializing {(Config.Video.Swap10Bit ? "10-bit" : "8-bit")} swap chain [Handle: {handle}, Buffers: {Config.Video.SwapBuffers}, Format: {(Config.Video.Swap10Bit ? Format.R10G10B10A2_UNorm : BGRA_OR_RGBA)}]"); diff --git a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.VideoProcessor.cs b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.VideoProcessor.cs index c75b837..679a275 100644 --- a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.VideoProcessor.cs +++ b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.VideoProcessor.cs @@ -407,7 +407,7 @@ void UpdateRotationUnSafe(uint angle, bool refresh = true) context.UpdateSubresource(vsBufferData, vsBuffer); vc?.VideoProcessorSetStreamRotation(vp, 0, true, _d3d11vpRotation); - UpdateAspectRatio(refresh); + UpdateAspectRatioUnSafe(refresh); } internal void UpdateAspectRatio(bool refresh = true) diff --git a/FlyleafLib/MediaFramework/MediaRenderer/ShaderCompiler.PixelShader.cs b/FlyleafLib/MediaFramework/MediaRenderer/ShaderCompiler.PixelShader.cs index 01814d2..c447eee 100644 --- a/FlyleafLib/MediaFramework/MediaRenderer/ShaderCompiler.PixelShader.cs +++ b/FlyleafLib/MediaFramework/MediaRenderer/ShaderCompiler.PixelShader.cs @@ -347,7 +347,7 @@ float4 main(PSInput input) : SV_TARGET c = Saturation(c, Config.saturation); #endif - return saturate(float4(c, color.a)); + return saturate(float4(c * color.a, color.a)); } "u8; } diff --git a/FlyleafLib/MediaFramework/MediaStream/VideoStream.cs b/FlyleafLib/MediaFramework/MediaStream/VideoStream.cs index 43d26c4..9e63aed 100644 --- a/FlyleafLib/MediaFramework/MediaStream/VideoStream.cs +++ b/FlyleafLib/MediaFramework/MediaStream/VideoStream.cs @@ -221,8 +221,11 @@ public void Refresh(VideoDecoder decoder, AVFrame* frame) // TBR: Can be filled } } - Width = (uint)(frame->width - (cropRect.Left + cropRect.Right)); - Height = (uint)(frame->height - (cropRect.Top + cropRect.Bottom)); + if (Width == 0) + { // Those are for info only (mainly before opening the stream, otherwise we get them from renderer at player's Video.X) + Width = (uint)codecCtx->width; + Height = (uint)codecCtx->height; + } if (frame->flags.HasFlag(FrameFlags.Interlaced)) FieldOrder = frame->flags.HasFlag(FrameFlags.TopFieldFirst) ? VideoFrameFormat.InterlacedTopFieldFirst : VideoFrameFormat.InterlacedBottomFieldFirst; diff --git a/FlyleafLib/MediaPlayer/Audio.cs b/FlyleafLib/MediaPlayer/Audio.cs index 97674be..ecb301f 100644 --- a/FlyleafLib/MediaPlayer/Audio.cs +++ b/FlyleafLib/MediaPlayer/Audio.cs @@ -281,7 +281,27 @@ internal void AddSamples(AudioFrame aFrame) } } internal long GetBufferedDuration() { lock (locker) { return (long) ((submittedSamples - sourceVoice.State.SamplesPlayed) * Timebase); } } - internal long GetDeviceDelay() { lock (locker) { return (long) ((xaudio2.PerformanceData.CurrentLatencyInSamples * Timebase) - 8_0000); } } // TODO: VBlack delay (8ms correction for now) + internal long GetDeviceDelay() + { + /* TODO + * Get rid of lockers during playback + * VBlack delay (8ms correction for now) + * TBR: (Very rare) Possible invalid response after quick Clear Buffers?* can return huge number ~60sec and can't even restore on next clear buffer + */ + lock (locker) + { + var latency = (long) ((xaudio2.PerformanceData.CurrentLatencyInSamples * Timebase) - 8_0000); + if (latency > TimeSpan.FromMilliseconds(500).Ticks) + { + #if DEBUG + player.Log.Error($"!!! Device Latency nosense -> {TicksToTimeMini(latency)}"); + #endif + return TimeSpan.FromMilliseconds(40).Ticks; + } + + return latency; + } + } internal void ClearBuffer() { lock (locker) diff --git a/FlyleafLib/MediaPlayer/Player.Extra.cs b/FlyleafLib/MediaPlayer/Player.Extra.cs index 48c2754..c91d14a 100644 --- a/FlyleafLib/MediaPlayer/Player.Extra.cs +++ b/FlyleafLib/MediaPlayer/Player.Extra.cs @@ -190,10 +190,10 @@ public void ShowFrameNext() SubtitleClear(i); } - if (VideoDecoder.Frames.IsEmpty) + if (vFrames.IsEmpty) vFrame = VideoDecoder.GetFrameNext(); else - VideoDecoder.Frames.TryDequeue(out vFrame); + vFrames.TryDequeue(out vFrame); if (vFrame == null) return; @@ -237,13 +237,13 @@ public void ShowFramePrev() SubtitleClear(i); } - if (VideoDecoder.Frames.IsEmpty) + if (vFrames.IsEmpty) { reversePlaybackResync = true; // Temp fix for previous timestamps until we seperate GetFrame for Extractor and the Player vFrame = VideoDecoder.GetFrame(VideoDecoder.GetFrameNumber(CurTime) - 1, true); } else - VideoDecoder.Frames.TryDequeue(out vFrame); + vFrames.TryDequeue(out vFrame); if (vFrame == null) return; diff --git a/FlyleafLib/MediaPlayer/Player.Open.cs b/FlyleafLib/MediaPlayer/Player.Open.cs index 7b215ad..50744ca 100644 --- a/FlyleafLib/MediaPlayer/Player.Open.cs +++ b/FlyleafLib/MediaPlayer/Player.Open.cs @@ -637,13 +637,13 @@ public StreamOpenedArgs Open(StreamBase stream, bool resync = true, bool default if (resync && (Duration > 0 || stream.Demuxer.IsHLSLive)) { - // Wait for at least on package before seek to update the HLS context first_time + // Wait for at least X packets before seek to update the HLS context first_time if (stream.Demuxer.IsHLSLive) { var curDemuxer = stream.Demuxer; int retries = 1000; // 20sec ? | For audio check also VideoPackets - while (stream.Demuxer.IsRunning && stream.Demuxer.GetPacketsPtr(stream).Count < 3 && stream.Demuxer.VideoPackets.Count < 3 && retries-- > 0) + while (stream.Demuxer.IsRunning && stream.Demuxer.GetPacketsPtr(stream).Count < 3 && retries-- > 0) Thread.Sleep(20); ReSync(stream, (int)((duration - fromEnd - (DateTime.UtcNow.Ticks - delay)) / 10000)); @@ -730,7 +730,7 @@ private Session GetCurrentSession() internal void ReSync(StreamBase stream, int syncMs = -1, bool accurate = false) { /* TODO - * + * TBR: Audio only might have issues * HLS live resync on stream switch should be from the end not from the start (could have different cache/duration) */ diff --git a/FlyleafLib/MediaPlayer/Player.Screamers.VASD.cs b/FlyleafLib/MediaPlayer/Player.Screamers.VASD.cs index b704ccb..dc073a3 100644 --- a/FlyleafLib/MediaPlayer/Player.Screamers.VASD.cs +++ b/FlyleafLib/MediaPlayer/Player.Screamers.VASD.cs @@ -11,9 +11,18 @@ unsafe partial class Player const int MAX_DEQUEUE_RETRIES = 20; // (Ms) Tries to avoid re-buffering by waiting the decoder to catch up volatile bool isScreamerVASDAudio; volatile bool stopScreamerVASDAudio; + long startTicks, lastSpeedChangeTicks; bool BufferVASD() { + long aDeviceDelay = 0; + bool gotAudio = !Audio.IsOpened || Config.Player.MaxLatency != 0; + bool gotVideo = false; + bool shouldStop = false; + bool showOneFrame = true; + int audioRetries = 4; + int loops = 0; + if (CanTrace) Log.Trace("Buffering"); while (isVideoSwitch && IsPlaying) Thread.Sleep(10); @@ -23,7 +32,7 @@ bool BufferVASD() if (Audio.isOpened && Config.Audio.Enabled) { - curAudioDeviceDelay = Audio.GetDeviceDelay(); + aDeviceDelay = Audio.GetDeviceDelay(); if (AudioDecoder.OnVideoDemuxer) AudioDecoder.Start(); @@ -69,21 +78,13 @@ bool BufferVASD() } } - VideoDecoder.DisposeFrame(vFrame); - vFrame = null; - aFrame = null; + if (vFrame != null && vFrame != renderer.LastFrame) // TBR: lock? + VideoDecoder.DisposeFrame(vFrame); + vFrame = null; aFrame = null; dFrame = null; for (int i = 0; i < subNum; i++) { sFrames[i] = null; } - dFrame = null; - - bool gotAudio = !Audio.IsOpened || Config.Player.MaxLatency != 0; - bool gotVideo = false; - bool shouldStop = false; - bool showOneFrame = true; - int audioRetries = 4; - int loops = 0; if (Config.Player.MaxLatency != 0) { @@ -96,10 +97,9 @@ bool BufferVASD() { loops++; - if (showOneFrame && !VideoDecoder.Frames.IsEmpty) + if (showOneFrame && !vFrames.IsEmpty) { ShowOneFrame(); - showOneFrameTicks = DateTime.UtcNow.Ticks; showOneFrame = false; } @@ -107,11 +107,8 @@ bool BufferVASD() if ((!showOneFrame || loops > 8) && !seeks.IsEmpty) return false; - if (!gotVideo && !showOneFrame && !VideoDecoder.Frames.IsEmpty) - { - VideoDecoder.Frames.TryDequeue(out vFrame); - if (vFrame != null) gotVideo = true; - } + if (!gotVideo && !showOneFrame && vFrames.TryDequeue(out vFrame)) + gotVideo = true; if (!gotAudio && aFrame == null && !AudioDecoder.Frames.IsEmpty) AudioDecoder.Frames.TryDequeue(out aFrame); @@ -126,7 +123,7 @@ bool BufferVASD() for (int i = 0; i < Math.Min(20, AudioDecoder.Frames.Count); i++) { if (aFrame == null - || aFrame.timestamp - curAudioDeviceDelay > vFrame.timestamp + || aFrame.timestamp - aDeviceDelay > vFrame.timestamp || vFrame.timestamp > duration) { gotAudio = true; @@ -220,6 +217,9 @@ void ScreamerVASD() bool secondField = false; // To be transfered in renderer int dequeueRetries; + int vDistanceMs, dDistanceMs; + int[] sDistanceMss = new int[subNum]; + long elapsedTicks; while (status == Status.Playing) { @@ -386,7 +386,7 @@ void ScreamerVASD() dequeueRetries = MAX_DEQUEUE_RETRIES; do { - while (!isVideoSwitch && !VideoDecoder.Frames.TryDequeue(out vFrame) && dequeueRetries-- > 0) + while (!isVideoSwitch && !vFrames.TryDequeue(out vFrame) && dequeueRetries-- > 0) Thread.Sleep(1); if (vFrame == null) @@ -431,7 +431,7 @@ void ScreamerVASD() // Present Current | Render Next if (CanTrace) Log.Trace($"[V] Presenting {TicksToTime(vFrame.timestamp)}{(secondField ? " | SF" : "")}"); - if (decoder.VideoDecoder.Renderer.PresentPlay()) + if (renderer.PresentPlay()) { framesDisplayed++; UpdateCurTime(vFrame.timestamp, false); @@ -457,7 +457,7 @@ void ScreamerVASD() vFrame = null; // don't dispose (LastFrame) secondField = false; dequeueRetries = MAX_DEQUEUE_RETRIES; - while (!isVideoSwitch && !VideoDecoder.Frames.TryDequeue(out vFrame) && dequeueRetries-- > 0) + while (!isVideoSwitch && !vFrames.TryDequeue(out vFrame) && dequeueRetries-- > 0) Thread.Sleep(1); if (vFrame != null && !renderer.RenderPlay(vFrame, secondField)) @@ -688,13 +688,13 @@ void ScreamerVASD() } private void CheckLatency() { - curLatency = GetBufferedDuration(); + long curLatency = GetBufferedDuration(); - if (CanDebug) Log.Debug($"[Latency {curLatency/10000}ms] Frames: {VideoDecoder.Frames.Count} Packets: {VideoDemuxer.VideoPackets.Count} Speed: {speed}"); + if (CanDebug) Log.Debug($"[Latency {curLatency/10000}ms] Frames: {vFrames.Count} Packets: {vPackets.Count} Speed: {speed}"); if (curLatency <= Config.Player.MinLatency) // We've reached the down limit (back to speed x1) { - ChangeSpeedWithoutBuffering(1); + ChangeSpeedWithoutBuffering(1, curLatency); return; } else if (curLatency < Config.Player.MaxLatency) @@ -711,9 +711,9 @@ private void CheckLatency() return; } - ChangeSpeedWithoutBuffering(newSpeed); + ChangeSpeedWithoutBuffering(newSpeed, curLatency); } - void ChangeSpeedWithoutBuffering(double newSpeed) + void ChangeSpeedWithoutBuffering(double newSpeed, long curLatency) { if (speed == newSpeed) return; @@ -735,15 +735,12 @@ void ChangeSpeedWithoutBuffering(double newSpeed) long GetBufferedDuration() // No speed aware => renderer.FieldType != VideoFrameFormat.Progressive && Config.Video.DoubleRate ? - (VideoDecoder.Frames.Count + VideoDemuxer.VideoPackets.Count) * renderer.VideoStream.FrameDuration2 : - (VideoDecoder.Frames.Count + VideoDemuxer.VideoPackets.Count) * renderer.VideoStream.FrameDuration; + (vFrames.Count + vPackets.Count) * renderer.VideoStream.FrameDuration2 : + (vFrames.Count + vPackets.Count) * renderer.VideoStream.FrameDuration; void ScreamerVASDAudio() { - long bufferTicks = 0; - long delayTicks = 0; - long elapsedTicks = 0; - long waitTicks = 0; + long bufferTicks, delayTicks, elapsedTicks, waitTicks; long desyncMs = 0; // use Ms to avoid rescale inaccuracy long expectingPts = NoTs; // Will be set on resync bool shouldResync = true; diff --git a/FlyleafLib/MediaPlayer/Player.Screamers.cs b/FlyleafLib/MediaPlayer/Player.Screamers.cs index 2f9a370..be6a7b3 100644 --- a/FlyleafLib/MediaPlayer/Player.Screamers.cs +++ b/FlyleafLib/MediaPlayer/Player.Screamers.cs @@ -51,22 +51,6 @@ protected virtual void OnBufferingCompleted(string error = null) long onBufferingStarted; long onBufferingCompleted; - int vDistanceMs; - int aDistanceMs; - int[] sDistanceMss; - int dDistanceMs; - int sleepMs; - - long elapsedTicks; - long startTicks; - long showOneFrameTicks; - - long lastSpeedChangeTicks; - long curLatency; - internal long curAudioDeviceDelay; - - public int subNum => Config.Subtitles.Max; - Stopwatch sw = new(); private void ShowOneFrame() @@ -75,7 +59,7 @@ private void ShowOneFrame() { sFrames[i] = null; } - if (!VideoDecoder.Frames.TryDequeue(out vFrame)) + if (!vFrames.TryDequeue(out vFrame)) return; renderer.RenderRequest(vFrame); @@ -237,6 +221,10 @@ private void ScreamerAudioOnly() private void ScreamerReverse() { // TBR: Timings / Rebuffer + + int vDistanceMs, sleepMs; + long elapsedTicks, startTicks = 0; + while (status == Status.Playing) { if (seeks.TryPop(out var seekData)) @@ -267,9 +255,9 @@ private void ScreamerReverse() VideoDecoder.Start(); // Recoding* - while (VideoDecoder.Frames.IsEmpty && status == Status.Playing && VideoDecoder.IsRunning) Thread.Sleep(15); + while (vFrames.IsEmpty && status == Status.Playing && VideoDecoder.IsRunning) Thread.Sleep(15); OnBufferingCompleted(); - if (!VideoDecoder.Frames.TryDequeue(out vFrame)) + if (!vFrames.TryDequeue(out vFrame)) { Log.Warn("No video frame"); break; } startTicks = vFrame.timestamp; @@ -310,7 +298,7 @@ private void ScreamerReverse() vFrame = null; int dequeueRetries = MAX_DEQUEUE_RETRIES; - while (!isVideoSwitch && !VideoDecoder.Frames.TryDequeue(out vFrame) && dequeueRetries-- > 0) + while (!isVideoSwitch && !vFrames.TryDequeue(out vFrame) && dequeueRetries-- > 0) Thread.Sleep(1); } if (vFrame != null) diff --git a/FlyleafLib/MediaPlayer/Player.cs b/FlyleafLib/MediaPlayer/Player.cs index 1a302d4..be0ab2f 100644 --- a/FlyleafLib/MediaPlayer/Player.cs +++ b/FlyleafLib/MediaPlayer/Player.cs @@ -72,6 +72,7 @@ public unsafe partial class Player : NotifyPropertyChanged, IDisposable /// (Normally you should not access this directly) /// public VideoDecoder VideoDecoder; + ConcurrentQueue vFrames; /// /// Player's Renderer @@ -125,6 +126,7 @@ internal void UpdateMainDemuxer() /// (Normally you should not access this directly) /// public Demuxer VideoDemuxer; + PacketQueue vPackets; /// /// Subtitles Demuxer @@ -211,9 +213,24 @@ public ObservableCollection Chapters => VideoDemuxer?.Chapters; /// - /// Player's current time or user's current seek time (uses backward direction or accurate seek based on Config.Player.SeekAccurate) + /// Player's current time or user's current seek time (useful for two-way slide bar binding) /// - public long CurTime { get => curTime; set { if (Config.Player.SeekAccurate) SeekAccurate((int) (value/10000)); else Seek((int) (value/10000), false); } } // Note: forward seeking casues issues to some formats and can have serious delays (eg. dash with h264, dash with vp9 works fine) + public long CurTime + { + get => curTime; + set + { + if (Config.Player.SeekAccurate) + SeekAccurate((int) (value/10000)); + else + // Note: forward seeking casues issues to some formats and can have serious delays (eg. dash with h264, dash with vp9 works fine) + // Seek forward only when not local file and we have a chance to find it in cache (we consider this comes from slide bars) + Seek((int)(value / 10000), + Playlist.InputType != InputType.File && Video.isOpened && + value > VideoDemuxer.CurTime && + value < VideoDemuxer.CurTime + VideoDemuxer.BufferedDuration - TimeSpan.FromMilliseconds(300).Ticks); + } + } internal long _CurTime, curTime; internal void SetCurTime() { @@ -504,6 +521,8 @@ public bool ReversePlayback volatile bool isAudioSwitch; volatile bool[] isSubsSwitches; volatile bool isDataSwitch; + + public int subNum => Config.Subtitles.Max; #endregion public Player(Config config = null) @@ -552,7 +571,11 @@ public Player(Config config = null) SubtitlesASR = decoder.SubtitlesASR; DataDemuxer = decoder.DataDemuxer; Playlist = decoder.Playlist; + + // We keep the same instance renderer = VideoDecoder.Renderer; + vFrames = VideoDecoder.Frames; + vPackets = VideoDemuxer.VideoPackets; UpdateMainDemuxer(); if (renderer != null) @@ -569,7 +592,6 @@ public Player(Config config = null) decoder.OpenExternalVideoStreamCompleted += Decoder_OpenExternalVideoStreamCompleted; decoder.OpenExternalSubtitlesStreamCompleted += Decoder_OpenExternalSubtitlesStreamCompleted; - //AudioDecoder.CBufAlloc = () => { if (aFrame != null) aFrame.dataPtr = IntPtr.Zero; aFrame = null; Audio.ClearBuffer(); aFrame = null; }; AudioDecoder.CodecChanged = Decoder_AudioCodecChanged; VideoDecoder.CodecChanged = Decoder_VideoCodecChanged; decoder.RecordingCompleted += (o, e) => { IsRecording = false; }; @@ -578,7 +600,6 @@ public Player(Config config = null) // second subtitles sFrames = new SubtitlesFrame[subNum]; sFramesPrev = new SubtitlesFrame[subNum]; - sDistanceMss = new int[subNum]; isSubsSwitches = new bool[subNum]; status = Status.Stopped; diff --git a/FlyleafLib/MediaPlayer/Video.cs b/FlyleafLib/MediaPlayer/Video.cs index 80b05b9..c01f079 100644 --- a/FlyleafLib/MediaPlayer/Video.cs +++ b/FlyleafLib/MediaPlayer/Video.cs @@ -146,10 +146,10 @@ internal void Refresh() pixelFormat = decoder.VideoStream.PixelFormatStr; framesTotal = decoder.VideoStream.TotalFrames; videoAcceleration - = decoder.VideoDecoder.VideoAccelerated; + = player.VideoDecoder.VideoAccelerated; hdrFormat = decoder.VideoStream.HDRFormat; colorFormat = $"{decoder.VideoStream.ColorSpace}\r\n{decoder.VideoStream.ColorTransfer}\r\n{decoder.VideoStream.ColorRange}"; - isOpened =!decoder.VideoDecoder.Disposed; + isOpened =!player.VideoDecoder.Disposed; player.ResetFrameStats(); bitRate = 0; diff --git a/FlyleafLib/Utils/NativeMethods.cs b/FlyleafLib/Utils/NativeMethods.cs index fcf0c94..1c5c3aa 100644 --- a/FlyleafLib/Utils/NativeMethods.cs +++ b/FlyleafLib/Utils/NativeMethods.cs @@ -7,6 +7,18 @@ public static partial class Utils { public static class NativeMethods { + public static WindowStyles SetWindowLong(nint hWnd, WindowStyles style) + => (WindowStyles)SetWindowLong(hWnd, (int)WindowLongFlags.GWL_STYLE, (nint)style); + + public static WindowStylesEx SetWindowLong(nint hWnd, WindowStylesEx style) + => (WindowStylesEx)SetWindowLong(hWnd, (int)WindowLongFlags.GWL_EXSTYLE, (nint)style); + + public static WindowStyles GetWindowLong(nint hWnd) + => (WindowStyles)GetWindowLong(hWnd, (int)WindowLongFlags.GWL_STYLE); + + public static WindowStylesEx GetWindowLongEx(nint hWnd) + => (WindowStylesEx)GetWindowLong(hWnd, (int)WindowLongFlags.GWL_EXSTYLE); + public static nint GetWindowLong(nint hWnd, int nIndex) => IntPtr.Size == 8 ? GetWindowLongPtr64(hWnd, nIndex) : GetWindowLongPtr32(hWnd, nIndex); @@ -14,16 +26,16 @@ public static nint SetWindowLong(nint hWnd, int nIndex, nint dwNewLong) => IntPtr.Size == 8 ? SetWindowLongPtr64(hWnd, nIndex, dwNewLong) : SetWindowLongPtr32(hWnd, nIndex, dwNewLong); [DllImport("user32.dll", EntryPoint = "GetWindowLong")] - public static extern nint GetWindowLongPtr32(nint hWnd, int nIndex); + static extern nint GetWindowLongPtr32(nint hWnd, int nIndex); [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")] - public static extern nint GetWindowLongPtr64(nint hWnd, int nIndex); + static extern nint GetWindowLongPtr64(nint hWnd, int nIndex); [DllImport("user32.dll", EntryPoint = "SetWindowLong")] - public static extern nint SetWindowLongPtr32(nint hWnd, int nIndex, nint dwNewLong); + static extern nint SetWindowLongPtr32(nint hWnd, int nIndex, nint dwNewLong); [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")] - public static extern nint SetWindowLongPtr64(nint hWnd, int nIndex, nint dwNewLong); + static extern nint SetWindowLongPtr64(nint hWnd, int nIndex, nint dwNewLong); [DllImport("user32.dll")] public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);