WPF佈局使用的是Panel族佈局控制項,它們均派生自Panel抽象類,主要用於控制UI佈局。 ...
在上一篇文章WinUI3 FFmpeg.autogen解析視頻幀,使用win2d顯示內容. - 吃飯/睡覺 - 博客園 (cnblogs.com) 里已經將整個視頻解碼的流程都實現了,現在我們來將整個播放視頻所需要的 播放,暫停,停止,和進度條功能都實現。
效果圖
一. 視頻跳轉進度到指定的時間。播放器的播放,暫停,停止這幾個功能都是控制視頻播放的狀態,蠻簡單的在這就不展開講了,但是視頻的時間跳轉就會稍微有點複雜,那麼我就從視頻時間跳轉這個功能進行展開講一下。
- ffmpeg.av_seek_frame()的函數參數說明
1)s : AVFormatContext類型的多媒體文件句柄
2)stream_index : int類型表示要進行操作的流索引
3)timestamp: long類型的時間戳,表示要跳轉到的時間位置
4.) flags :跳轉方法,主要有以下幾種:
AVSEEK_FLAG_BACKWARD : 跳轉到時間戳之前的最近關鍵幀
AVSEEK_FLAG_BYTE 基於位元組位置的跳轉
AVSEEK_FLAG_ANY 跳轉到任意幀,不一定是關鍵幀
AVSEEK_FLAG_FRAME 基於幀數量的跳轉
參數說明引用於:FFMPEG av_seek_frame - 知乎 (zhihu.com)
2.SeekProgress() 設置視頻進度,參數為秒數,將視頻到跳轉到指定的時間。
public void SeekProgress(int seekTime) { if (format == null || videoStream == null) return; lock (SyncLock) { IsPlaying = false;//將視頻暫停播放 clock.Stop(); //將秒數轉換成視頻的時間戳 var timestamp = seekTime / ffmpeg.av_q2d(videoStream->time_base); //將媒體容器裡面的指定流(視頻)的時間戳設置到指定的位置,並指定跳轉的方法; ffmpeg.av_seek_frame(format, videoStreamIndex, (long)timestamp, ffmpeg.AVSEEK_FLAG_BACKWARD | ffmpeg.AVSEEK_FLAG_FRAME); ffmpeg.av_frame_unref(frame);//清除上一幀的數據 ffmpeg.av_packet_unref(packet); //清除上一幀的數據包 int error = 0; //迴圈獲取幀數據,判斷獲取的幀時間戳已經大於給定的時間戳則說明已經到達了指定的位置則退出迴圈 while (packet->pts < timestamp) { do { do { ffmpeg.av_packet_unref(packet);//清除上一幀數據包 error = ffmpeg.av_read_frame(format, packet);//讀取數據 if (error == ffmpeg.AVERROR_EOF)//是否是到達了視頻的結束位置 return; } while (packet->stream_index != videoStreamIndex);//判斷當前獲取的數據是否是視頻數據 ffmpeg.avcodec_send_packet(codecContext, packet);//將數據包發送給解碼器解碼 error = ffmpeg.avcodec_receive_frame(codecContext, frame);//從解碼器獲取解碼後的幀數據 } while (error == ffmpeg.AVERROR(ffmpeg.EAGAIN)); } OffsetClock = TimeSpan.FromSeconds(seekTime);//設置時間偏移 clock.Restart();//時鐘從新開始 IsPlaying = true;//視頻開始播放 lastTime = TimeSpan.Zero; } }
在代碼里雖然我們已經調用了ffmpeg.av_seek_frame() 跳轉到指定的時間戳了,但是這個函數並不會準確的跳轉到我們想要的位置,因為它會幫我們跳轉到指定位置的前最近關鍵幀,所以我們需要寫一個while迴圈來不停的讀取每一幀將不符合我們想要的幀都丟棄直到獲取到我們指定的位置才結束。
二. DecodecVideo
我將上一篇解碼視頻的整個流程和上面跳轉視頻位置的代碼都封裝成了DecodecVideo 類,在類裡面添加了 Play(),Pause(),Stop(),SeekProgress()函數以便於更加容易的控制視頻播放。
public unsafe class DecodecVideo : IMedia { //媒體格式上下文(媒體容器) AVFormatContext* format; //編解碼上下文 AVCodecContext* codecContext; //媒體數據包 AVPacket* packet; //媒體幀數據 AVFrame* frame; //圖像轉換器 SwsContext* convert; //視頻流 AVStream* videoStream; // 視頻流在媒體容器上流的索引 int videoStreamIndex; TimeSpan OffsetClock; //幀,數據指針 IntPtr FrameBufferPtr; byte_ptrArray4 TargetData; int_array4 TargetLinesize; object SyncLock = new object(); //時鐘 Stopwatch clock = new Stopwatch(); //播放上一幀的時間 TimeSpan lastTime; bool isNextFrame = true; public event MediaHandler MediaCompleted; public event MediaHandler MediaPlay; public event MediaHandler MediaPause; #region //視頻時長 public TimeSpan Duration { get; protected set; } //編解碼器名字 public string CodecName { get; protected set; } public string CodecId { get; protected set; } //比特率 public int Bitrate { get; protected set; } //幀率 public double FrameRate { get; protected set; } //圖像的高和款 public int FrameWidth { get; protected set; } public int FrameHeight { get; protected set; } //是否是正在播放中 public bool IsPlaying { get; protected set; } public MediaState State { get; protected set; } public TimeSpan Position { get => clock.Elapsed + OffsetClock; } //一幀顯示時長 public TimeSpan frameDuration { get; private set; } #endregion /// <summary> /// 初始化解碼視頻 /// </summary> /// <param name="path"></param> public void InitDecodecVideo(string path) { int error = 0; //創建一個 媒體格式上下文 format = ffmpeg.avformat_alloc_context(); if (format == null) { Debug.WriteLine("創建媒體格式(容器)失敗"); return; } var tempFormat = format; //打開視頻 error = ffmpeg.avformat_open_input(&tempFormat, path, null, null); if (error < 0) { Debug.WriteLine("打開視頻失敗"); return; } //獲取流信息 ffmpeg.avformat_find_stream_info(format, null); //編解碼器類型 AVCodec* codec = null; //獲取視頻流索引 videoStreamIndex = ffmpeg.av_find_best_stream(format, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0); if (videoStreamIndex < 0) { Debug.WriteLine("沒有找到視頻流"); return; } //根據流索引找到視頻流 videoStream = format->streams[videoStreamIndex]; //創建解碼器上下文 codecContext = ffmpeg.avcodec_alloc_context3(codec); //將視頻流裡面的解碼器參數設置到 解碼器上下文中 error = ffmpeg.avcodec_parameters_to_context(codecContext, videoStream->codecpar); if (error < 0) { Debug.WriteLine("設置解碼器參數失敗"); return; } //打開解碼器 error = ffmpeg.avcodec_open2(codecContext, codec, null); if (error < 0) { Debug.WriteLine("打開解碼器失敗"); return; } //視頻時長等視頻信息 //Duration = TimeSpan.FromMilliseconds(videoStream->duration / ffmpeg.av_q2d(videoStream->time_base)); Duration = TimeSpan.FromMilliseconds(format->duration / 1000); CodecId = videoStream->codecpar->codec_id.ToString(); CodecName = ffmpeg.avcodec_get_name(videoStream->codecpar->codec_id); Bitrate = (int)videoStream->codecpar->bit_rate; FrameRate = ffmpeg.av_q2d(videoStream->r_frame_rate); FrameWidth = videoStream->codecpar->width; FrameHeight = videoStream->codecpar->height; frameDuration = TimeSpan.FromMilliseconds(1000 / FrameRate); //初始化轉換器,將圖片從源格式 轉換成 BGR0 (8:8:8)格式 var result = InitConvert(FrameWidth, FrameHeight, codecContext->pix_fmt, FrameWidth, FrameHeight, AVPixelFormat.AV_PIX_FMT_BGR0); //所有內容都初始化成功了開啟時鐘,用來記錄時間 if (result) { //從記憶體中分配控制項給 packet 和frame packet = ffmpeg.av_packet_alloc(); frame = ffmpeg.av_frame_alloc(); } } /// <summary> /// 初始化轉換器 /// </summary> /// <param name="sourceWidth">源寬度</param> /// <param name="sourceHeight">源高度</param> /// <param name="sourceFormat">源格式</param> /// <param name="targetWidth">目標高度</param> /// <param name="targetHeight">目標寬度</param> /// <param name="targetFormat">目標格式</param> /// <returns></returns> bool InitConvert(int sourceWidth, int sourceHeight, AVPixelFormat sourceFormat, int targetWidth, int targetHeight, AVPixelFormat targetFormat) { //根據輸入參數和輸出參數初始化轉換器 convert = ffmpeg.sws_getContext(sourceWidth, sourceHeight, sourceFormat, targetWidth, targetHeight, targetFormat, ffmpeg.SWS_FAST_BILINEAR, null, null, null); if (convert == null) { Debug.WriteLine("創建轉換器失敗"); return false; } //獲取轉換後圖像的 緩衝區大小 var bufferSize = ffmpeg.av_image_get_buffer_size(targetFormat, targetWidth, targetHeight, 1); //創建一個指針 FrameBufferPtr = Marshal.AllocHGlobal(bufferSize); TargetData = new byte_ptrArray4(); TargetLinesize = new int_array4(); ffmpeg.av_image_fill_arrays(ref TargetData, ref TargetLinesize, (byte*)FrameBufferPtr, targetFormat, targetWidth, targetHeight, 1); return true; } public byte[] FrameConvertBytes(AVFrame* sourceFrame) { // 利用轉換器將yuv 圖像數據轉換成指定的格式數據 ffmpeg.sws_scale(convert, sourceFrame->data, sourceFrame->linesize, 0, sourceFrame->height, TargetData, TargetLinesize); var data = new byte_ptrArray8(); data.UpdateFrom(TargetData); var linesize = new int_array8(); linesize.UpdateFrom(TargetLinesize); //創建一個位元組數據,將轉換後的數據從記憶體中讀取成位元組數組 byte[] bytes = new byte[FrameWidth * FrameHeight * 4]; Marshal.Copy((IntPtr)data[0], bytes, 0, bytes.Length); return bytes; } public bool TryReadNextFrame(out AVFrame outFrame) { if (lastTime == TimeSpan.Zero) { lastTime = Position; isNextFrame = true; } else { if (Position - lastTime >= frameDuration) { lastTime = Position; isNextFrame = true; } else { outFrame = *frame; return false; } } if (isNextFrame) { lock (SyncLock) { int result = -1; //清理上一幀的數據 ffmpeg.av_frame_unref(frame); while (true) { //清理上一幀的數據包 ffmpeg.av_packet_unref(packet); //讀取下一幀,返回一個int 查看讀取數據包的狀態 result = ffmpeg.av_read_frame(format, packet); //讀取了最後一幀了,沒有數據了,退出讀取幀 if (result == ffmpeg.AVERROR_EOF || result < 0) { outFrame = *frame; StopPlay(); return false; } //判斷讀取的幀數據是否是視頻數據,不是則繼續讀取 if (packet->stream_index != videoStreamIndex) continue; //將包數據發送給解碼器解碼 ffmpeg.avcodec_send_packet(codecContext, packet); //從解碼器中接收解碼後的幀 result = ffmpeg.avcodec_receive_frame(codecContext, frame); if (result < 0) continue; outFrame = *frame; return true; } } } else { outFrame = *frame; return false; } } void StopPlay() { lock (SyncLock) { if (State == MediaState.None) return; IsPlaying = false; OffsetClock = TimeSpan.FromSeconds(0); clock.Reset(); clock.Stop(); var tempFormat = format; ffmpeg.avformat_free_context(tempFormat); format = null; var tempCodecContext = codecContext; ffmpeg.avcodec_free_context(&tempCodecContext); var tempPacket = packet; ffmpeg.av_packet_free(&tempPacket); var tempFrame = frame; ffmpeg.av_frame_free(&tempFrame); var tempConvert = convert; ffmpeg.sws_freeContext(convert); videoStream = null; videoStreamIndex = -1; //視頻時長 Duration = TimeSpan.FromMilliseconds(0); //編解碼器名字 CodecName = String.Empty; CodecId = String.Empty; //比特率 Bitrate = 0; //幀率 FrameRate = 0; //圖像的高和款 FrameWidth = 0; FrameHeight = 0; State = MediaState.None; Marshal.FreeHGlobal(FrameBufferPtr); lastTime = TimeSpan.Zero; MediaCompleted?.Invoke(Duration); } } /// <summary> /// 更改進度 /// </summary> /// <param name="seekTime">更改到的位置(秒)</param> public void SeekProgress(int seekTime) { if (format == null || videoStream == null) return; lock (SyncLock) { IsPlaying = false;//將視頻暫停播放 clock.Stop(); //將秒數轉換成視頻的時間戳 var timestamp = seekTime / ffmpeg.av_q2d(videoStream->time_base); //將媒體容器裡面的指定流(視頻)的時間戳設置到指定的位置,並指定跳轉的方法; ffmpeg.av_seek_frame(format, videoStreamIndex, (long)timestamp, ffmpeg.AVSEEK_FLAG_BACKWARD | ffmpeg.AVSEEK_FLAG_FRAME); ffmpeg.av_frame_unref(frame);//清除上一幀的數據 ffmpeg.av_packet_unref(packet); //清除上一幀的數據包 int error = 0; //迴圈獲取幀數據,判斷獲取的幀時間戳已經大於給定的時間戳則說明已經到達了指定的位置則退出迴圈 while (packet->pts < timestamp) { do { do { ffmpeg.av_packet_unref(packet);//清除上一幀數據包 error = ffmpeg.av_read_frame(format, packet);//讀取數據 if (error == ffmpeg.AVERROR_EOF)//是否是到達了視頻的結束位置 return; } while (packet->stream_index != videoStreamIndex);//判斷當前獲取的數據是否是視頻數據 ffmpeg.avcodec_send_packet(codecContext, packet);//將數據包發送給解碼器解碼 error = ffmpeg.avcodec_receive_frame(codecContext, frame);//從解碼器獲取解碼後的幀數據 } while (error == ffmpeg.AVERROR(ffmpeg.EAGAIN)); } OffsetClock = TimeSpan.FromSeconds(seekTime);//設置時間偏移 clock.Restart();//時鐘從新開始 IsPlaying = true;//視頻開始播放 lastTime = TimeSpan.Zero; } } public void Play() { if (State == MediaState.Play) return; clock.Start(); IsPlaying = true; State = MediaState.Play; } public void Pause() { if (State != MediaState.Play) return; IsPlaying = false; OffsetClock = clock.Elapsed; clock.Stop(); clock.Reset(); State = MediaState.Pause; } public void Stop() { if (State == MediaState.None) return; StopPlay(); } }
public enum MediaState { //沒有播放 None, Read, Play, Pause, } public interface IMedia { public delegate void MediaHandler(TimeSpan duration); public event MediaHandler MediaCompleted; }
三. 界面
<Grid> <Grid.Resources> <Style TargetType="TextBlock" x:Key="Key"> <Setter Property="control:DockPanel.Dock" Value="Left" /> <Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="FontWeight" Value="Bold" /> <Setter Property="FontSize" Value="15"></Setter> <Setter Property="Foreground" Value="White"></Setter> <Setter Property="Width" Value="80" /> </Style> <Style TargetType="TextBlock" x:Key="Value"> <Setter Property="control:DockPanel.Dock" Value="Right" /> <Setter Property="HorizontalAlignment" Value="Stretch" /> <Setter Property="FontWeight" Value="Normal" /> <Setter Property="FontSize" Value="15"></Setter> <Setter Property="Foreground" Value="White"></Setter> </Style> </Grid.Resources> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition Height="auto"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition ></ColumnDefinition> <ColumnDefinition Width="auto"></ColumnDefinition> </Grid.ColumnDefinitions> <canvas:CanvasControl x:Name="canvas"></canvas:CanvasControl> <StackPanel Background="Black" Grid.Column="1" Width="200"> <control:DockPanel> <TextBlock Text="Duration" Style="{StaticResource Key}"></TextBlock> <TextBlock x:Name="dura" Text="00:00:00" Style="{StaticResource Value}"></TextBlock> </control:DockPanel> <control:DockPanel> <TextBlock Text="Position" Style="{StaticResource Key}"></TextBlock> <TextBlock x:Name="position" Text="00:00:00" Style="{StaticResource Value}"></TextBlock> </control:DockPanel> <control:DockPanel Background="LightBlue"> <TextBlock Style="{StaticResource Key}">Has Video</TextBlock> <TextBlock Style="{StaticResource Value}" /> </control:DockPanel> <control:DockPanel > <TextBlock Style="{StaticResource Key}" Text="Video Codec"></TextBlock> <TextBlock Style="{StaticResource Value}" x:Name="videoCodec" /> </control:DockPanel> <control:DockPanel > <TextBlock Style="{StaticResource Key}" Text="Video Bitrate"></TextBlock> <TextBlock Style="{StaticResource Value}" x:Name="videoBitrate" /> </control:DockPanel> <control:DockPanel > <TextBlock Style="{StaticResource Key}" Text="Video Width"></TextBlock> <TextBlock Style="{StaticResource Value}" x:Name="videoWidth"/> </control:DockPanel> <control:DockPanel > <TextBlock Style="{StaticResource Key}" Text="Video Height"></TextBlock> <TextBlock Style="{StaticResource Value}" x:Name="videoHeight" /> </control:DockPanel> <control:DockPanel > <TextBlock Style="{StaticResource Key}" Text="Video FPS"></TextBlock> <TextBlock Style="{StaticResource Value}" x:Name="videoFps" /> </control:DockPanel> </StackPanel> <StackPanel Grid.Row="1" Grid.ColumnSpan="2"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="auto"></ColumnDefinition> <ColumnDefinition ></ColumnDefinition> <ColumnDefinition Width="auto"></ColumnDefinition> </Grid.ColumnDefinitions> <TextBlock Text="{Binding ElementName=position,Path=Text,Mode=OneWay}"></TextBlock> <Slider Grid.Column="1" x:Name="progress"></Slider> <TextBlock Grid.Column="2