ffmpeg.autogen库中有两个函数ffmpeg.av_read_pause和ffmpeg.av_read_play,再配合是否处于暂停中的标识进行控制循环,从而达到暂停和继续播放的目的。下面是示例代码:
/// <summary>
/// 对读取的264数据包进行解码和转换
/// </summary>
/// <param name="show">解码完成回调函数</param>
/// <param name="url">播放地址,也可以是本地文件地址</param>
public unsafe void Start(ShowBitmap show, string url, out string strResult)
{
strResult = "";
try
{
CanRun = true;
#region ffmpeg 日志
// 设置记录ffmpeg日志级别
ffmpeg.av_log_set_level(ffmpeg.AV_LOG_VERBOSE);
av_log_set_callback_callback logCallback = (p0, level, format, vl) =>
{
if (level > ffmpeg.av_log_get_level()) return;
var lineSize = 1024;
var lineBuffer = stackalloc byte[lineSize];
var printPrefix = 1;
ffmpeg.av_log_format_line(p0, level, format, vl, lineBuffer, lineSize, &printPrefix);
var line = Marshal.PtrToStringAnsi((IntPtr)lineBuffer);
Console.Write(line);
};
ffmpeg.av_log_set_callback(logCallback);
#endregion
#region ffmpeg 转码
// 分配音视频格式上下文
pFormatContext = ffmpeg.avformat_alloc_context();
var pFormatContextTemp = pFormatContext;
// 增加缓冲区大小
//pFormatContextTemp->probesize = 100 * 1024 * 1024; // 100 MB
//pFormatContextTemp->max_analyze_duration = 5 * ffmpeg.AV_TIME_BASE; // 5 seconds
int error;
//打开流
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
AVDictionary* options = null;
ffmpeg.av_dict_set(&options, "rtsp_transport", "tcp", 0);//指定拉流网络类型
error = ffmpeg.avformat_open_input(&pFormatContextTemp, url, null, &options);
if (error != 0) throw new ApplicationException(GetErrorMessage(error));
stopwatch.Stop();
Console.Write($"打开流 使用时间:{stopwatch.ElapsedMilliseconds}");
// 读取媒体流信息
stopwatch = new Stopwatch();
stopwatch.Start();
error = ffmpeg.avformat_find_stream_info(pFormatContext, null);
if (error != 0) throw new ApplicationException(GetErrorMessage(error));
stopwatch.Stop();
Console.Write($"读取媒体流信息 使用时间:{stopwatch.ElapsedMilliseconds}");
// 这里只是为了打印些视频参数
//AVDictionaryEntry* tag = null;
//while ((tag = ffmpeg.av_dict_get(pFormatContext->metadata, "", tag, ffmpeg.AV_DICT_IGNORE_SUFFIX)) != null)
//{
// var key = Marshal.PtrToStringAnsi((IntPtr)tag->key);
// var value = Marshal.PtrToStringAnsi((IntPtr)tag->value);
// //Console.WriteLine($"{key} = {value}");
//}
// 从格式化上下文获取流索引
AVStream* pStream = null, aStream;
for (var i = 0; i < pFormatContext->nb_streams; i++)
{
if (pFormatContext->streams[i]->codec->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
{
streamIndex = i;
pStream = pFormatContext->streams[i];
}
else if (pFormatContext->streams[i]->codec->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO)
{
aStream = pFormatContext->streams[i];
}
}
if (streamIndex == -1 || pStream == null) throw new ApplicationException(@"Could not found video stream.");
if (error < 0) throw new ApplicationException(GetErrorMessage(error));
// 获取流的编码器上下文
var codecContext = *pStream->codec;
//Console.WriteLine($"codec name: {ffmpeg.avcodec_get_name(codecContext.codec_id)}");
// 获取图像的宽、高及像素格式
var width = codecContext.width;
var height = codecContext.height;
var sourcePixFmt = codecContext.pix_fmt;
// 得到编码器ID
var codecId = codecContext.codec_id;
// 目标像素格式
var destinationPixFmt = AVPixelFormat.AV_PIX_FMT_BGR24;
// 某些264格式codecContext.pix_fmt获取到的格式是AV_PIX_FMT_NONE 统一都认为是YUV420P
if (sourcePixFmt == AVPixelFormat.AV_PIX_FMT_NONE && codecId == AVCodecID.AV_CODEC_ID_H264)
{
sourcePixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P;
}
// 得到SwsContext对象:用于图像的缩放和转换操作
pConvertContext = ffmpeg.sws_getContext(width, height, sourcePixFmt,
width, height, destinationPixFmt,
ffmpeg.SWS_FAST_BILINEAR, null, null, null);
if (pConvertContext == null) throw new ApplicationException(@"Could not initialize the conversion context.");
//分配一个默认的帧对象:AVFrame
pConvertedFrame = ffmpeg.av_frame_alloc();
// 目标媒体格式需要的字节长度
var convertedFrameBufferSize = ffmpeg.av_image_get_buffer_size(destinationPixFmt, width, height, 1);
// 分配目标媒体格式内存使用
convertedFrameBufferPtr = Marshal.AllocHGlobal(convertedFrameBufferSize);
var dstData = new byte_ptrArray4();
var dstLinesize = new int_array4();
// 设置图像填充参数
ffmpeg.av_image_fill_arrays(ref dstData, ref dstLinesize, (byte*)convertedFrameBufferPtr, destinationPixFmt, width, height, 1);
#endregion
#region ffmpeg 解码
// 根据编码器ID获取对应的解码器
var pCodec = ffmpeg.avcodec_find_decoder(codecId);
if (pCodec == null) throw new ApplicationException(@"Unsupported codec.");
pCodecContext = &codecContext;
if ((pCodec->capabilities & ffmpeg.AV_CODEC_CAP_TRUNCATED) == ffmpeg.AV_CODEC_CAP_TRUNCATED)
pCodecContext->flags |= ffmpeg.AV_CODEC_FLAG_TRUNCATED;
// 通过解码器打开解码器上下文:AVCodecContext pCodecContext
error = ffmpeg.avcodec_open2(pCodecContext, pCodec, null);
// 丢弃所有的非关键帧
pCodecContext->skip_frame = AVDiscard.AVDISCARD_NONKEY;
// 初始化媒体数据包
var pPacket = new AVPacket();
packet = &pPacket;
var frameNumber = 0;
while (CanRun)
{
lock (objLock)
{
if (!_isPause)
{
try
{
do
{
// 读取一帧未解码数据
error = ffmpeg.av_read_frame(pFormatContext, packet);
if (error == ffmpeg.AVERROR_EOF) break;
if (error < 0) throw new ApplicationException(GetErrorMessage(error));
if (packet->stream_index != pStream->index) continue;
// 解码
error = ffmpeg.avcodec_send_packet(pCodecContext, packet);
if (error < 0) throw new ApplicationException(GetErrorMessage(error));
// 解码输出解码数据
error = ffmpeg.avcodec_receive_frame(pCodecContext, pConvertedFrame);
} while (error == ffmpeg.AVERROR(ffmpeg.EAGAIN) && CanRun);
if (error == ffmpeg.AVERROR_EOF) continue;
if (error < 0) throw new ApplicationException(GetErrorMessage(error));
if (packet->stream_index != pStream->index) continue;
// YUV->RGB
ffmpeg.sws_scale(pConvertContext, pConvertedFrame->data, pConvertedFrame->linesize, 0, height, dstData, dstLinesize);
}
finally
{
ffmpeg.av_packet_unref(packet);//释放数据包对象引用
ffmpeg.av_frame_unref(pConvertedFrame);//释放解码帧对象引用
}
// 封装Bitmap图片
var bitmap = new Bitmap(width, height, dstLinesize[0], PixelFormat.Format24bppRgb, convertedFrameBufferPtr);
// 回调
show(bitmap);
frameNumber++;
}
}
}
//播放完置空播放图片
show(null);
Thread.Sleep(200);
#region 释放资源
Marshal.FreeHGlobal(convertedFrameBufferPtr);
ffmpeg.sws_freeContext(pConvertContext);
ffmpeg.av_free(pConvertedFrame);
ffmpeg.avcodec_close(pCodecContext);
ffmpeg.avformat_close_input(&pFormatContextTemp);
#endregion
#endregion
}
catch (Exception ex)
{
strResult = "-1";
//_log.Error($"解码异常,详细信息:{ex.Message}");
}
}
暂停的时候将_isPause=true,继续播放的时候设置_isPause=false。
/// <summary>
/// 暂停
/// </summary>
public void Pause()
{
if (_isPause)
return;
lock (objLock)
{
_isPause = true;
Task.Run(() => { Thread.Sleep(20); ffmpeg.av_read_pause(pFormatContext); });
//ffmpeg.av_packet_unref(packet);
//ffmpeg.av_frame_unref(pConvertedFrame);//释放解码帧对象引用
}
}
/// <summary>
/// 继续播放
/// </summary>
public void ContinuePlay()
{
if (!_isPause)
return;
Task.Run(() =>
{
ffmpeg.av_read_play(pFormatContext);
Thread.Sleep(100);
_isPause = false;
});
}
暂停和继续播放方法中用了task多线程,因为不用的话在暂停或继续播放时偶尔会卡死。