在上一篇文章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 = https://www.cnblogs.com/chifan/p/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=https://www.cnblogs.com/chifan/p/"Left" />
<Setter Property="HorizontalAlignment" Value=https://www.cnblogs.com/chifan/p/"Left" />
<Setter Property="FontWeight" Value=https://www.cnblogs.com/chifan/p/"Bold" />
<Setter Property="FontSize" Value=https://www.cnblogs.com/chifan/p/"15"></Setter>
<Setter Property="Foreground" Value=https://www.cnblogs.com/chifan/p/"White"></Setter>
<Setter Property="Width" Value=https://www.cnblogs.com/chifan/p/"80" />
</Style>
<Style TargetType="TextBlock" x:Key="Value">
<Setter Property="control:DockPanel.Dock" Value=https://www.cnblogs.com/chifan/p/"Right" />
<Setter Property="HorizontalAlignment" Value=https://www.cnblogs.com/chifan/p/"Stretch" />
<Setter Property="FontWeight" Value=https://www.cnblogs.com/chifan/p/"Normal" />
<Setter Property="FontSize" Value=https://www.cnblogs.com/chifan/p/"15"></Setter>
<Setter Property="Foreground" Value=https://www.cnblogs.com/chifan/p/"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" Text="00:00:00" x:Name="duration"></TextBlock>
</Grid>
<TextBox x:Name="pathBox" Text="C:\Users\ludin\Desktop\新建檔案夾 (4)\1.mp4" PlaceholderText="地址輸入"></TextBox>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<Button x:Name="play" >播放</Button>
<Button x:Name="pause" >暫停</Button>
<Button x:Name="stop" >停止</Button>
</StackPanel>
</StackPanel>
</Grid>
在界面中間位置上放置了一個 CanvasControl 用于繪制視頻內容,右邊放置了用于顯示視頻的資訊的文本控制元件,在下方放置了用于控制視頻播放和暫停的幾個按鈕和進度條,
四.后代代碼
public unsafe sealed partial class FFmpegDecodecVideo : Page { Task PlayTask; CanvasBitmap bitmap; DispatcherTimer timer = new DispatcherTimer(); bool progressActivity = false; DecodecVideo video = new DecodecVideo(); public FFmpegDecodecVideo() { this.InitializeComponent(); Init(); InitUi(); } void Init() { //播放 play.Click += (s, e) => { if (video.State == MediaState.None) { //初始化解碼視頻 video.InitDecodecVideo(pathBox.Text); DisplayVideoInfo(); } video.Play(); timer.Start(); }; //暫停 pause.Click += (s, e) => video.Pause(); stop.Click += (s, e) => video.Stop(); ; PlayTask = new Task(() => { while (true) { //播放中 if (video.IsPlaying) { //獲取下一幀視頻 if (video.TryReadNextFrame(out var frame)) { var bytes = video.FrameConvertBytes(&frame); bitmap = CanvasBitmap.CreateFromBytes(CanvasDevice.GetSharedDevice(), bytes, video.FrameWidth, video.FrameHeight, DirectXPixelFormat.B8G8R8A8UIntNormalized); canvas.Invalidate(); } } } }); PlayTask.Start(); video.MediaCompleted += (s) => { DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () => { timer.Stop(); progressActivity = false; DisplayVideoInfo(); }); }; } void InitUi() { //畫布繪制 canvas.Draw += (s, e) => { if (bitmap != null) { var te = Win2DUlit.CalcutateImageCenteredTransform(canvas.ActualSize, bitmap.Size); te.Source = bitmap; e.DrawingSession.DrawImage(te); } }; timer.Interval = TimeSpan.FromMilliseconds(300); //計時器更新進度條 timer.Tick += (s, e) => { if (!video.IsPlaying) return; position.Text = video.Position.ToString(); progressActivity = false; progress.Value = video.Position.TotalSeconds; progressActivity = true; }; //進度條更改 progress.ValueChanged += (s, e) => { if (!video.IsPlaying) return; if (progressActivity == true) { video.SeekProgress((int)e.NewValue); } }; } /// <summary> /// 顯示視頻資訊 /// </summary> void DisplayVideoInfo() { dura.Text = video.Duration.ToString(); videoCodec.Text = video.CodecName; videoBitrate.Text = video.Bitrate.ToString(); videoWidth.Text = video.FrameWidth.ToString(); videoHeight.Text = video.FrameHeight.ToString(); videoFps.Text = video.FrameRate.ToString(); duration.Text = video.Duration.ToString(); position.Text = video.Position.ToString(); progress.Maximum = video.Duration.TotalSeconds; } }
五. 結語
在這里,我們已經將一個普通的播放視頻的播放器就寫完了,有了播放,暫停,停止,更改進度的功能,下一篇文章我將展示如何通過ffmpeg解碼音頻用 NAudio播放音頻;
專案Demo地址:LearnFFmppeg: 學習和記錄ffmpeg - Gitee.com
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/500092.html
標籤:C#
