概述
本文首先以 FFmpeg 視頻解碼為主題训唱,主要介紹了 FFmpeg 進(jìn)行解碼視頻時(shí)的主要流程、基本原理;其次至壤,文章還講述了與 FFmpeg 視頻解碼有關(guān)的簡(jiǎn)單應(yīng)用,包括如何在原有的 FFmpeg 視頻解碼的基礎(chǔ)上按照一定時(shí)間軸順序播放視頻枢纠、如何在播放視頻時(shí)加入 seek 的邏輯像街;除此之外,文章重點(diǎn)介紹了解碼視頻時(shí)可能容易遺漏的細(xì)節(jié)晋渺,最后是簡(jiǎn)單地闡述了下如何封裝一個(gè)具有基本的視頻解碼功能的 VideoDecoder镰绎。
前言
FFmpeg
FFmpeg 是一套可以用來錄制、轉(zhuǎn)換數(shù)字音頻木西、視頻畴栖,并能將其轉(zhuǎn)化為流的開源計(jì)算機(jī)程序,它可生成用于處理和操作多媒體數(shù)據(jù)的庫(kù)八千,其中包含了先進(jìn)的音視頻解碼庫(kù)libavcodec和音視頻格式轉(zhuǎn)換庫(kù)libavformat吗讶。
FFmpeg 六大常用功能模塊
libavformat:多媒體文件或協(xié)議的封裝和解封裝庫(kù),如 mp4恋捆、flv 等文件封裝格式照皆,rtmp、rtsp 等網(wǎng)絡(luò)協(xié)議封裝格式沸停;
libavcodec:音視頻解碼核心庫(kù)膜毁;
libavfilter:音視頻、字幕濾鏡庫(kù);
libswscale:圖像格式轉(zhuǎn)換庫(kù)瘟滨;
libswresample:音頻重采樣庫(kù)葬凳;
libavutil:工具庫(kù)
視頻解碼基礎(chǔ)入門
解復(fù)用(Demux):解復(fù)用也可叫解封裝。這里有一個(gè)概念叫封裝格式室奏,封裝格式指的是音視頻的組合格式火焰,常見的有 mp4、flv胧沫、mkv 等昌简。通俗來講,封裝是將音頻流绒怨、視頻流纯赎、字幕流以及其他附件按一定規(guī)則組合成一個(gè)封裝的產(chǎn)物。而解封裝起著與封裝相反的作用南蹂,將一個(gè)流媒體文件拆解成音頻數(shù)據(jù)和視頻數(shù)據(jù)等犬金。此時(shí)拆分后數(shù)據(jù)是經(jīng)過壓縮編碼的,常見的視頻壓縮數(shù)據(jù)格式有 h264六剥。
解碼(Decode):簡(jiǎn)單來說晚顷,就是對(duì)壓縮的編碼數(shù)據(jù)解壓成原始的視頻像素?cái)?shù)據(jù),常用的原始視頻像素?cái)?shù)據(jù)格式有 yuv疗疟。
色彩空間轉(zhuǎn)換(Color Space Convert):通常對(duì)于圖像顯示器來說该默,它是通過 RGB 模型來顯示圖像的,但在傳輸圖像數(shù)據(jù)時(shí)使用 YUV 模型可以節(jié)省帶寬策彤。因此在顯示圖像時(shí)就需要將 yuv 像素格式的數(shù)據(jù)轉(zhuǎn)換成 rgb 的像素格式后再進(jìn)行渲染栓袖。
渲染(Render):將前面已經(jīng)解碼和進(jìn)行色彩空間轉(zhuǎn)換的每一個(gè)視頻幀的數(shù)據(jù)發(fā)送給顯卡以繪制在屏幕畫面上。
相關(guān)視頻推薦
<u>https</u><u>://ke.qq.com/course/3202131?flowToken=1042316</u>
一店诗、 引入 FFmpeg 前的準(zhǔn)備工作
1.1 FFmpeg so 庫(kù)編譯
在 FFmpeg 官網(wǎng)下載源碼庫(kù)并解壓裹刮;
下載 NDK 庫(kù)并解壓;
配置解壓后的 FFmpeg 源碼庫(kù)目錄中的 configure庞瘸,修改高亮部分幾個(gè)參數(shù)為以下的內(nèi)容捧弃,主要目的是生成 Android 可使用的 名稱-版本.so 文件的格式;
# ······
# build settings
SHFLAGS='-shared -Wl,-soname,$$(@F)'
LIBPREF="lib"
LIBSUF=".a"
FULLNAME='$(NAME)$(BUILDSUF)'
LIBNAME='$(LIBPREF)$(FULLNAME)$(LIBSUF)'
SLIBPREF="lib"
SLIBSUF=".so"
SLIBNAME='$(SLIBPREF)$(FULLNAME)$(SLIBSUF)'
SLIBNAME_WITH_VERSION='$(SLIBNAME).$(LIBVERSION)'
# 已修改配置
SLIBNAME_WITH_MAJOR='$(SLIBNAME)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'
# ······
在 FFmpeg 源碼庫(kù)目錄下新建腳本文件 build_android_arm_v8a.sh恕洲,在文件中配置 NDK 的路徑塔橡,并輸入下面其他的內(nèi)容;
# 清空上次的編譯
make clean
# 這里先配置你的 NDK 路徑
export NDK=/Users/bytedance/Library/Android/sdk/ndk/21.4.7075529
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64
function build_android
{
./configure \
--prefix=$PREFIX \
--disable-postproc \
--disable-debug \
--disable-doc \
--enable-FFmpeg \
--disable-doc \
--disable-symver \
--disable-static \
--enable-shared \
--cross-prefix=$CROSS_PREFIX \
--target-os=android \
--arch=$ARCH \
--cpu=$CPU \
--cc=$CC \
--cxx=$CXX \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $OPTIMIZE_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS"
make clean
make -j16
make install
echo "============================ build android arm64-v8a success =========================="
}
# arm64-v8a
ARCH=arm64
CPU=armv8-a
API=21
CC=$TOOLCHAIN/bin/aarch64-linux-android$API-clang
CXX=$TOOLCHAIN/bin/aarch64-linux-android$API-clang++
SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysroot
CROSS_PREFIX=$TOOLCHAIN/bin/aarch64-linux-android-
PREFIX=$(pwd)/android/$CPU
OPTIMIZE_CFLAGS="-march=$CPU"
echo $CC
build_android
設(shè)置 NDK 文件夾中所有文件的權(quán)限 chmod 777 -R NDK霜第;
終端執(zhí)行腳本 ./build_android_arm_v8a.sh葛家,開始編譯 FFmpeg。編譯成功后的文件會(huì)在 FFmpeg 下的 android 目錄中泌类,會(huì)出現(xiàn)多個(gè) .so 文件癞谒;
若要編譯 arm-v7a底燎,只需要拷貝修改以上的腳本為以下 build_android_arm_v7a.sh 的內(nèi)容。
#armv7-a
ARCH=arm
CPU=armv7-a
API=21
CC=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang
CXX=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang++
SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysroot
CROSS_PREFIX=$TOOLCHAIN/bin/arm-linux-androideabi-
PREFIX=$(pwd)/android/$CPU
OPTIMIZE_CFLAGS="-mfloat-abi=softfp -mfpu=vfp -marm -march=$CPU "
分享一個(gè)音視頻高級(jí)開發(fā)交流群,需要學(xué)習(xí)資料的點(diǎn)擊788280672*加入群自取,資料包括(C/C++弹砚,Linux双仍,F(xiàn)Fmpeg webRTC rtmp hls rtsp ffplay srs 等等),免費(fèi)分享桌吃。
1.2 在 Android 中引入 FFmpeg 的 so 庫(kù)
NDK 環(huán)境朱沃、CMake 構(gòu)建工具、LLDB(C/C++ 代碼調(diào)試工具)茅诱;
新建 C++ module逗物,一般會(huì)生成以下幾個(gè)重要的文件:CMakeLists.txt、native-lib.cpp瑟俭、MainActivity翎卓;
在 app/src/main/ 目錄下,新建目錄摆寄,并命名 jniLibs失暴,這是 Android Studio 默認(rèn)放置 so 動(dòng)態(tài)庫(kù)的目錄;接著在 jniLibs 目錄下微饥,新建 arm64-v8a 目錄逗扒,然后將編譯好的 .so 文件粘貼至此目錄下;然后再將編譯時(shí)生成的 .h 頭文件(FFmpeg 對(duì)外暴露的接口)粘貼至 cpp 目錄下的 include 中畜号。以上的 .so 動(dòng)態(tài)庫(kù)目錄和 .h 頭文件目錄都會(huì)在 CMakeLists.txt 中顯式聲明和鏈接進(jìn)來缴阎;
最上層的 MainActivity,在這里面加載 C/C++ 代碼編譯的庫(kù):native-lib简软。native-lib 在 CMakeLists.txt 中被添加到名為 "ffmpeg" 的 library 中,所以在 System.loadLibrary()中輸入的是 "ffmpeg"述暂;
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Example of a call to a native method
sample_text.text = stringFromJNI()
}
// 聲明一個(gè)外部引用的方法痹升,此方法和 C/C++ 層的代碼是對(duì)應(yīng)的。
external fun stringFromJNI(): String
companion object {
// 在 init{} 中加載 C/C++ 編譯成的 library:ffmpeg
// library 名稱的定義和添加在 CMakeLists.txt 中完成
init {
System.loadLibrary("ffmpeg")
}
}
}
native-lib.cpp 是一個(gè) C++ 接口文件畦韭,Java 層中聲明的 external 方法在這里得到實(shí)現(xiàn)疼蛾;
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_bytedance_example_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
CMakeLists.txt 是一個(gè)構(gòu)建腳本,目的是配置可以編譯出 native-lib 此 so 庫(kù)的構(gòu)建信息艺配;
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.10.2)
# Declares and names the project.
project("ffmpeg")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
# 定義 so 庫(kù)和頭文件所在目錄察郁,方便后面使用
set(FFmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
set(FFmpeg_head_dir ${CMAKE_SOURCE_DIR}/FFmpeg)
# 添加頭文件目錄
include_directories(
FFmpeg/include
)
add_library( # Sets the name of the library.
ffmmpeg
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.cpp
)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
# 添加FFmpeg相關(guān)的so庫(kù)
add_library( avutil
SHARED
IMPORTED )
set_target_properties( avutil
PROPERTIES IMPORTED_LOCATION
${FFmpeg_lib_dir}/libavutil.so )
add_library( swresample
SHARED
IMPORTED )
set_target_properties( swresample
PROPERTIES IMPORTED_LOCATION
${FFmpeg_lib_dir}/libswresample.so )
add_library( avcodec
SHARED
IMPORTED )
set_target_properties( avcodec
PROPERTIES IMPORTED_LOCATION
${FFmpeg_lib_dir}/libavcodec.so )
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
audioffmmpeg
# 把前面添加進(jìn)來的 FFmpeg.so 庫(kù)都鏈接到目標(biāo)庫(kù) native-lib 上
avutil
swresample
avcodec
-landroid
# Links the target library to the log library
# included in the NDK.
${log-lib})
以上的操作就將 FFmpeg 引入 Android 項(xiàng)目。
二转唉、FFmpeg 解碼視頻的原理和細(xì)節(jié)
2.1 主要流程
2.2 基本原理
2.2.1 常用的 ffmpeg 接口
// 1 分配 AVFormatContext
avformat_alloc_context();
// 2 打開文件輸入流
avformat_open_input(AVFormatContext **ps, const char *url,
const AVInputFormat *fmt, AVDictionary **options);
// 3 提取輸入文件中的數(shù)據(jù)流信息
avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
// 4 分配編解碼上下文
avcodec_alloc_context3(const AVCodec *codec);
// 5 基于與數(shù)據(jù)流相關(guān)的編解碼參數(shù)來填充編解碼器上下文
avcodec_parameters_to_context(AVCodecContext *codec,
const AVCodecParameters *par);
// 6 查找對(duì)應(yīng)已注冊(cè)的編解碼器
avcodec_find_decoder(enum AVCodecID id);
// 7 打開編解碼器
avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
// 8 不停地從碼流中提取壓縮幀數(shù)據(jù)皮钠,獲取的是一幀視頻的壓縮數(shù)據(jù)
av_read_frame(AVFormatContext *s, AVPacket *pkt);
// 9 發(fā)送原生的壓縮數(shù)據(jù)輸入到解碼器(compressed data)
avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
// 10 接收解碼器輸出的解碼數(shù)據(jù)
avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
2.2.2 視頻解碼的整體思路
首先要注冊(cè) libavformat 并且注冊(cè)所有的編解碼器、復(fù)用/解復(fù)用組赠法、協(xié)議等麦轰。它是所有基于 FFmpeg 的應(yīng)用程序中第一個(gè)被調(diào)用的函數(shù), 只有調(diào)用了該函數(shù),才能正常使用 FFmpeg 的各項(xiàng)功能。另外款侵,在最新版本的 FFmpeg 中目前已經(jīng)可以不用加入這行代碼末荐;
av_register_all();
打開視頻文件,提取文件中的數(shù)據(jù)流信息新锈;
auto av_format_context = avformat_alloc_context();
avformat_open_input(&av_format_context, path_.c_str(), nullptr, nullptr);
avformat_find_stream_info(av_format_context, nullptr);
然后獲取視頻媒體流的下標(biāo)甲脏,才能找到文件中的視頻媒體流;
int video_stream_index = -1;
for (int i = 0; i < av_format_context->nb_streams; i++) {
// 匹配找到視頻媒體流的下標(biāo)妹笆,
if (av_format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_index = i;
LOGD(TAG, "find video stream index = %d", video_stream_index);
break;
}
}
獲取視頻媒體流剃幌、獲取解碼器上下文、獲取解碼器上下文晾浴、配置解碼器上下文的參數(shù)值负乡、打開解碼器;
// 獲取視頻媒體流
auto stream = av_format_context->streams[video_stream_index];
// 找到已注冊(cè)的解碼器
auto codec = avcodec_find_decoder(stream->codecpar->codec_id);
// 獲取解碼器上下文
AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
// 將視頻媒體流的參數(shù)配置到解碼器上下文
auto ret = avcodec_parameters_to_context(codec_ctx, stream->codecpar);
if (ret >= 0) {
// 打開解碼器
avcodec_open2(codec_ctx, codec, nullptr);
// ······
}
通過指定像素格式脊凰、圖像寬抖棘、圖像高來計(jì)算所需緩沖區(qū)需要的內(nèi)存大小,分配設(shè)置緩沖區(qū)狸涌;并且由于是上屏繪制切省,因此我們需要用到 ANativeWindow,使用 ANativeWindow_setBuffersGeometry 設(shè)置此繪制窗口的屬性帕胆;
video_width_ = codec_ctx->width;
video_height_ = codec_ctx->height;
int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGBA,
video_width_, video_height_, 1);
// 輸出 buffer
out_buffer_ = (uint8_t*) av_malloc(buffer_size * sizeof(uint8_t));
// 通過設(shè)置寬高來限制緩沖區(qū)中的像素?cái)?shù)量朝捆,而非顯示屏幕的尺寸。
// 如果緩沖區(qū)與顯示的屏幕尺寸不相符懒豹,則實(shí)際顯示的可能會(huì)是拉伸芙盘,或者被壓縮的圖像
int result = ANativeWindow_setBuffersGeometry(native_window_, video_width_,
video_height_, WINDOW_FORMAT_RGBA_8888);
分配內(nèi)存空間給像素格式為 RGBA 的 AVFrame,用于存放轉(zhuǎn)換成 RGBA 后的幀數(shù)據(jù)脸秽;設(shè)置 rgba_frame 緩沖區(qū)儒老,使其與 out_buffer_ 相關(guān)聯(lián);
auto rgba_frame = av_frame_alloc();
av_image_fill_arrays(rgba_frame->data, rgba_frame->linesize,
out_buffer_,
AV_PIX_FMT_RGBA,
video_width_, video_height_, 1);
獲取 SwsContext记餐,它在調(diào)用 sws_scale() 進(jìn)行圖像格式轉(zhuǎn)換和圖像縮放時(shí)會(huì)使用到驮樊。YUV420P 轉(zhuǎn)換為 RGBA 時(shí)可能會(huì)在調(diào)用 sws_scale 時(shí)格式轉(zhuǎn)換失敗而無法返回正確的高度值,原因跟調(diào)用 sws_getContext 時(shí) flags 有關(guān)片酝,需要將 SWS_BICUBIC 換成 SWS_FULL_CHR_H_INT | SWS_ACCURATE_RND囚衔;
struct SwsContext* data_convert_context = sws_getContext(
video_width_, video_height_, codec_ctx->pix_fmt,
video_width_, video_height_, AV_PIX_FMT_RGBA,
SWS_BICUBIC, nullptr, nullptr, nullptr);
分配內(nèi)存空間給用于存儲(chǔ)原始數(shù)據(jù)的 AVFrame,指向原始幀數(shù)據(jù)雕沿;并且分配內(nèi)存空間給用于存放視頻解碼前數(shù)據(jù)的 AVPacket练湿;
auto frame = av_frame_alloc();
auto packet = av_packet_alloc();
從視頻碼流中循環(huán)讀取壓縮幀數(shù)據(jù),然后開始解碼晦炊;
ret = av_read_frame(av_format_context, packet);
if (packet->size) {
Decode(codec_ctx, packet, frame, stream, lock, data_convert_context, rgba_frame);
}
在 Decode() 函數(shù)中將裝有原生壓縮數(shù)據(jù)的 packet 作為輸入發(fā)送給解碼器鞠鲜;
/* send the packet with the compressed data to the decoder */
ret = avcodec_send_packet(codec_ctx, pkt);
解碼器返回解碼后的幀數(shù)據(jù)到指定的 frame 上宁脊,后續(xù)可對(duì)已解碼 frame 的 pts 換算為時(shí)間戳,按時(shí)間軸的顯示順序逐幀繪制到播放的畫面上贤姆;
while (ret >= 0 && !is_stop_) {
// 返回解碼后的數(shù)據(jù)到 frame
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return;
} else if (ret < 0) {
return;
}
// 拿到當(dāng)前解碼后的 frame榆苞,對(duì)其 pts 換算成時(shí)間戳,以便于跟傳入的指定時(shí)間戳進(jìn)行比
auto decode_time_ms = frame->pts * 1000 / stream->time_base.den;
if (decode_time_ms >= time_ms_) {
last_decode_time_ms_ = decode_time_ms;
is_seeking_ = false;
// ······
// 圖片數(shù)據(jù)格式轉(zhuǎn)換
// ······
// 把轉(zhuǎn)換后的數(shù)據(jù)繪制到屏幕上
}
av_packet_unref(pkt);
}
繪制畫面之前霞捡,要進(jìn)行圖片數(shù)據(jù)格式的轉(zhuǎn)換坐漏,這里就要用到前面獲取到的 SwsContext;
// 圖片數(shù)據(jù)格式轉(zhuǎn)換
int result = sws_scale(
sws_context,
(const uint8_t* const*) frame->data, frame->linesize,
0, video_height_,
rgba_frame->data, rgba_frame->linesize);
if (result <= 0) {
LOGE(TAG, "Player Error : data convert fail");
return;
}
因?yàn)槭巧掀晾L制碧信,所以用到了 ANativeWindow 和 ANativeWindow_Buffer赊琳。在繪制畫面之前,需要使用鎖定窗口的下一個(gè)繪圖 surface 以進(jìn)行繪制砰碴,然后將要顯示的幀數(shù)據(jù)寫入到緩沖區(qū)中躏筏,最后解鎖窗口的繪圖 surface,將緩沖區(qū)的數(shù)據(jù)發(fā)布到屏幕顯示上呈枉;
// 播放
result = ANativeWindow_lock(native_window_, &window_buffer_, nullptr);
if (result < 0) {
LOGE(TAG, "Player Error : Can not lock native window");
} else {
// 將圖像繪制到界面上
// 注意 : 這里 rgba_frame 一行的像素和 window_buffer 一行的像素長(zhǎng)度可能不一致
// 需要轉(zhuǎn)換好 否則可能花屏
auto bits = (uint8_t*) window_buffer_.bits;
for (int h = 0; h < video_height_; h++) {
memcpy(bits + h * window_buffer_.stride * 4,
out_buffer_ + h * rgba_frame->linesize[0],
rgba_frame->linesize[0]);
}
ANativeWindow_unlockAndPost(native_window_);
}
以上就是主要的解碼過程趁尼。除此之外,因?yàn)?C++ 使用資源和內(nèi)存空間時(shí)需要自行釋放猖辫,所以解碼結(jié)束后還需要調(diào)用釋放的接口釋放資源酥泞,以免造成內(nèi)存泄漏。
sws_freeContext(data_convert_context);
av_free(out_buffer_);
av_frame_free(&rgba_frame);
av_frame_free(&frame);
av_packet_free(&packet);
avcodec_close(codec_ctx);
avcodec_free_context(&codec_ctx);
avformat_close_input(&av_format_context);
avformat_free_context(av_format_context);
ANativeWindow_release(native_window_);
2.3 簡(jiǎn)單應(yīng)用
為了更好地理解視頻解碼的過程啃憎,這里封裝一個(gè)視頻解碼器 VideoDecoder 芝囤,解碼器初步會(huì)有以下幾個(gè)函數(shù):
VideoDecoder(const char* path, std::function<void(long timestamp)> on_decode_frame);
void Prepare(ANativeWindow* window);
bool DecodeFrame(long time_ms);
void Release();
在這個(gè)視頻解碼器中,輸入指定時(shí)間戳后會(huì)返回解碼的這一幀數(shù)據(jù)辛萍。其中較為重要的是 DecodeFrame(long time_ms) 函數(shù)悯姊,它可以由使用者自行調(diào)用,傳入指定幀的時(shí)間戳叹阔,進(jìn)而解碼對(duì)應(yīng)的幀數(shù)據(jù)挠轴。此外,可以增加同步鎖以實(shí)現(xiàn)解碼線程和使用線程分離耳幢。
2.3.1 加入同步鎖實(shí)現(xiàn)視頻播放
若只要對(duì)視頻進(jìn)行解碼,是不需要使用同步等待的欧啤;
但若是要實(shí)現(xiàn)視頻的播放睛藻,那么每解碼繪制完一幀就需使用鎖進(jìn)行同步等待,這是因?yàn)椴シ乓曨l時(shí)需要讓解碼和繪制分離邢隧、且按照一定的時(shí)間軸順序和速度進(jìn)行解碼和繪制店印。
condition_.wait(lock);
在上層調(diào)用 DecodeFrame 函數(shù)傳入解碼的時(shí)間戳?xí)r喚醒同步鎖,讓解碼繪制的循環(huán)繼續(xù)執(zhí)行倒慧。
bool VideoDecoder::DecodeFrame(long time_ms) {
// ······
time_ms_ = time_ms;
condition_.notify_all();
return true;
}
2.3.2 播放時(shí)加入 seek_frame
在正常播放情況下按摘,視頻是一幀一幀逐幀解碼播放包券;但在拖動(dòng)進(jìn)度條到達(dá)指定的 seek 點(diǎn)的情況下,如果還是從頭到尾逐幀解碼到 seek 點(diǎn)的話炫贤,效率可能不太高溅固。這時(shí)候就需要在一定規(guī)則內(nèi)對(duì) seek 點(diǎn)的時(shí)間戳做檢查,符合條件的直接 seek 到指定的時(shí)間戳兰珍。
FFmpeg 中的 av_seek_frame
av_seek_frame 可以定位到關(guān)鍵幀和非關(guān)鍵幀侍郭,這取決于選擇的 flag 值。因?yàn)橐曨l的解碼需要依賴關(guān)鍵幀掠河,所以一般我們需要定位到關(guān)鍵幀亮元;
int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp,
int flags);
av_seek_frame 中的 flag 是用來指定尋找的 I 幀和傳入的時(shí)間戳之間的位置關(guān)系。當(dāng)要 seek 已過去的時(shí)間戳?xí)r唠摹,時(shí)間戳不一定會(huì)剛好處在 I 幀的位置爆捞,但因?yàn)榻獯a需要依賴 I 幀,所以需要先找到此時(shí)間戳附近一個(gè)的 I 幀勾拉,此時(shí) flag 就表明要 seek 到當(dāng)前時(shí)間戳的前一個(gè) I 幀還是后一個(gè) I 幀煮甥;
flag 有四個(gè)選項(xiàng):
flag 可能同時(shí)包含以上的多個(gè)值。比如 AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_BYTE望艺;
FRAME 和 BACKWARD 是按幀之間的間隔推算出 seek 的目標(biāo)位置苛秕,適合快進(jìn)快退;BYTE 則適合大幅度滑動(dòng)找默。
seek 的場(chǎng)景
解碼時(shí)傳入的時(shí)間戳若是往前進(jìn)的方向艇劫,并且超過上一幀時(shí)間戳有一定距離就需要 seek,這里的“一定距離”是通過多次實(shí)驗(yàn)估算所得惩激,并非都是以下代碼中使用的 1000ms店煞;
如果是往后退的方向且小于上一次解碼時(shí)間戳,但與上一次解碼時(shí)間戳的距離比較大(比如已超過 50ms)风钻,就要 seek 到上一個(gè)關(guān)鍵幀顷蟀;
使用 bool 變量 is_seeking_ 是為了防止其他干擾當(dāng)前 seeking 的操作,目的是控制當(dāng)前只有一個(gè) seek 操作在進(jìn)行骡技。
if (!is_seeking_ && (time_ms_ > last_decode_time_ms_ + 1000 ||
time_ms_ < last_decode_time_ms_ - 50)) {
is_seeking_ = true;
// seek 時(shí)傳入的是指定幀帶有 time_base 的時(shí)間戳鸣个,因此要用 times_ms 進(jìn)行推算
LOGD(TAG, "seek frame time_ms_ = %ld, last_decode_time_ms_ = %ld", time_ms_,
last_decode_time_ms_);
av_seek_frame(av_format_context,
video_stream_index,
time_ms_ * stream->time_base.den / 1000,
AVSEEK_FLAG_BACKWARD);
}
插入 seek 的邏輯
因?yàn)樵诮獯a前要檢查是否 seek布朦,所以要在 av_read_frame 函數(shù)(返回視頻媒體流下一幀)之前插入 seek 的邏輯囤萤,符合 seek 條件時(shí)使用 av_seek_frame 到達(dá)指定 I 幀,接著 av_read_frame 后再繼續(xù)解碼到目的時(shí)間戳的位置是趴。
// 是否進(jìn)行 seek 的邏輯寫在這
// 接下來是讀取視頻流的下一幀
int ret = av_read_frame(av_format_context, packet);
2.4 解碼過程中的細(xì)節(jié)
2.4.1 DecodeFrame 時(shí) seek 的條件
使用 av_seek_frame 函數(shù)時(shí)需要指定正確的 flag涛舍,并且還要約定進(jìn)行 seek 操作時(shí)的條件,否則視頻可能會(huì)出現(xiàn)花屏(馬賽克)唆途。
if (!is_seeking_ && (time_ms_ > last_decode_time_ms_ + 1000 ||
time_ms_ < last_decode_time_ms_ - 50)) {
is_seeking_ = true;
av_seek_frame(···,···,···,AVSEEK_FLAG_BACKWARD);
}
2.4.2 減少解碼的次數(shù)
在視頻解碼時(shí)富雅,在有些條件下是可以不用對(duì)傳入時(shí)間戳的幀數(shù)據(jù)進(jìn)行解碼的掸驱。比如:
當(dāng)前解碼時(shí)間戳若是前進(jìn)方向并且與上一次的解碼時(shí)間戳相同或者與當(dāng)前正在解碼的時(shí)間戳相同,則不需要進(jìn)行解碼没佑;
當(dāng)前解碼時(shí)間戳若不大于上一次的解碼時(shí)間戳并且與上一次的解碼時(shí)間戳之間的距離相差較斜显簟(比如未超過 50ms),則不需要進(jìn)行解碼图筹。
bool VideoDecoder::DecodeFrame(long time_ms) {
LOGD(TAG, "DecodeFrame time_ms = %ld", time_ms);
if (last_decode_time_ms_ == time_ms || time_ms_ == time_ms) {
LOGD(TAG, "DecodeFrame last_decode_time_ms_ == time_ms");
return false;
}
if (time_ms <= last_decode_time_ms_ &&
time_ms + 50 >= last_decode_time_ms_) {
return false;
}
time_ms_ = time_ms;
condition_.notify_all();
return true;
}
有了以上這些條件的約束后帅刀,會(huì)減少一些不必要的解碼操作。
2.4.3 使用 AVFrame 的 pts
AVPacket 存儲(chǔ)解碼前的數(shù)據(jù)(編碼數(shù)據(jù):H264/AAC 等)远剩,保存的是解封裝之后扣溺、解碼前的數(shù)據(jù),仍然是壓縮數(shù)據(jù)瓜晤;AVFrame 存儲(chǔ)解碼后的數(shù)據(jù)(像素?cái)?shù)據(jù):YUV/RGB/PCM 等)锥余;
AVPacket 的 pts 和 AVFrame 的 pts 意義存在差異。前者表示這個(gè)解壓包何時(shí)顯示痢掠,后者表示幀數(shù)據(jù)何時(shí)顯示驱犹;
// AVPacket 的 pts
/**
* Presentation timestamp in AVStream->time_base units; the time at which
* the decompressed packet will be presented to the user.
* Can be AV_NOPTS_VALUE if it is not stored in the file.
* pts MUST be larger or equal to dts as presentation cannot happen before
* decompression, unless one wants to view hex dumps. Some formats misuse
* the terms dts and pts/cts to mean something different. Such timestamps
* must be converted to true pts/dts before they are stored in AVPacket.
*/
int64_t pts;
// AVFrame 的 pts
/**
* Presentation timestamp in time_base units (time when frame should be shown to user).
*/
int64_t pts;
是否將當(dāng)前解碼的幀數(shù)據(jù)繪制到畫面上,取決于傳入到解碼時(shí)間戳與當(dāng)前解碼器返回的已解碼幀的時(shí)間戳的比較結(jié)果足画。這里不可使用 AVPacket 的 pts雄驹,它很可能不是一個(gè)遞增的時(shí)間戳;
需要進(jìn)行畫面繪制的前提是:當(dāng)傳入指定的解碼時(shí)間戳不大于當(dāng)前已解碼 frame 的 pts 換算后的時(shí)間戳?xí)r進(jìn)行畫面繪制淹辞。
auto decode_time_ms = frame->pts * 1000 / stream->time_base.den;
LOGD(TAG, "decode_time_ms = %ld", decode_time_ms);
if (decode_time_ms >= time_ms_) {
last_decode_time_ms_ = decode_time_ms;
is_seeking = false;
// 畫面繪制
// ····
}
2.4.4 解碼最后一幀時(shí)視頻已經(jīng)沒有數(shù)據(jù)
使用 av_read_frame(av_format_context, packet)返回視頻媒體流下一幀到 AVPacket 中医舆。如果函數(shù)返回的 int 值是 0 則是 Success,如果小于 0 則是 Error 或者 EOF象缀。
因此如果在播放視頻時(shí)返回的是小于 0 的值蔬将,調(diào)用 avcodec_flush_buffers 函數(shù)重置解碼器的狀態(tài),flush 緩沖區(qū)中的內(nèi)容央星,然后再 seek 到當(dāng)前傳入的時(shí)間戳處霞怀,完成解碼后的回調(diào),再讓同步鎖進(jìn)行等待莉给。
// 讀取碼流中的音頻若干幀或者視頻一幀毙石,
// 這里是讀取視頻一幀(完整的一幀),獲取的是一幀視頻的壓縮數(shù)據(jù)颓遏,接下來才能對(duì)其進(jìn)行解碼
ret = av_read_frame(av_format_context, packet);
if (ret < 0) {
avcodec_flush_buffers(codec_ctx);
av_seek_frame(av_format_context, video_stream_index,
time_ms_ * stream->time_base.den / 1000, AVSEEK_FLAG_BACKWARD);
LOGD(TAG, "ret < 0, condition_.wait(lock)");
// 防止解最后一幀時(shí)視頻已經(jīng)沒有數(shù)據(jù)
on_decode_frame_(last_decode_time_ms_);
condition_.wait(lock);
}
2.5 上層封裝解碼器 VideoDecoder
如果要在上層封裝一個(gè) VideoDecoder胁黑,只需要將 C++ 層 VideoDecoder 的接口暴露在 native-lib.cpp 中,然后上層通過 JNI 的方式調(diào)用 C++ 的接口州泊。
比如上層要傳入指定的解碼時(shí)間戳進(jìn)行解碼時(shí),寫一個(gè) deocodeFrame 方法漂洋,然后把時(shí)間戳傳到 C++ 層的 nativeDecodeFrame 進(jìn)行解碼遥皂,而 nativeDecodeFrame 這個(gè)方法的實(shí)現(xiàn)就寫在 native-lib.cpp 中力喷。
// FFmpegVideoDecoder.kt
class FFmpegVideoDecoder(
path: String,
val onDecodeFrame: (timestamp: Long, texture: SurfaceTexture, needRender: Boolean) -> Unit
){
// 抽第 timeMs 幀,根據(jù) sync 是否同步等待
fun decodeFrame(timeMS: Long, sync: Boolean = false) {
// 若當(dāng)前不需要抽幀時(shí)不進(jìn)行等待
if (nativeDecodeFrame(decoderPtr, timeMS) && sync) {
// ······
} else {
// ······
}
}
private external fun nativeDecodeFrame(decoder: Long, timeMS: Long): Boolean
companion object {
const val TAG = "FFmpegVideoDecoder"
init {
System.loadLibrary("ffmmpeg")
}
}
}
然后在 native-lib.cpp 中調(diào)用 C++ 層 VideoDecoder 的接口 DecodeFrame 演训,這樣就通過 JNI 的方式建立起了上層和 C++ 底層之間的聯(lián)系
// native-lib.cpp
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_example_decoder_video_FFmpegVideoDecoder_nativeDecodeFrame(JNIEnv* env,
jobject thiz,
jlong decoder,
jlong time_ms) {
auto videoDecoder = (codec::VideoDecoder*)decoder;
return videoDecoder->DecodeFrame(time_ms);
}
三弟孟、心得
技術(shù)經(jīng)驗(yàn)
FFmpeg 編譯后與 Android 結(jié)合起來實(shí)現(xiàn)視頻的解碼播放,便捷性很高样悟。
由于是用 C++ 層實(shí)現(xiàn)具體的解碼流程拂募,會(huì)有學(xué)習(xí)難度,最好有一定的 C++ 基礎(chǔ)窟她。
四陈症、附錄
C++ 封裝的 VideoDecoder
VideoDecoder.h
#include <jni.h>
#include <mutex>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <time.h>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
#include <libswscale/swscale.h>
}
#include <string>
/*
* VideoDecoder 可用于解碼某個(gè)音視頻文件(比如.mp4)中視頻媒體流的數(shù)據(jù)。
* Java 層傳入指定文件的路徑后震糖,可以按一定 fps 循環(huán)傳入指定的時(shí)間戳進(jìn)行解碼(抽幀)录肯,這一實(shí)現(xiàn)由 C++ 提供的 DecodeFrame 來完成。
* 在每次解碼結(jié)束時(shí)吊说,將解碼某一幀的時(shí)間戳回調(diào)給上層的解碼器论咏,以供其他操作使用。
*/
namespace codec {
class VideoDecoder {
private:
std::string path_;
long time_ms_ = -1;
long last_decode_time_ms_ = -1;
bool is_seeking_ = false;
ANativeWindow* native_window_ = nullptr;
ANativeWindow_Buffer window_buffer_{};颁井、
// 視頻寬高屬性
int video_width_ = 0;
int video_height_ = 0;
uint8_t* out_buffer_ = nullptr;
// on_decode_frame 用于將抽取指定幀的時(shí)間戳回調(diào)給上層解碼器厅贪,以供上層解碼器進(jìn)行其他操作。
std::function<void(long timestamp)> on_decode_frame_ = nullptr;
bool is_stop_ = false;
// 會(huì)與在循環(huán)同步時(shí)用的鎖 “std::unique_lock<std::mutex>” 配合使用
std::mutex work_queue_mtx;
// 真正在進(jìn)行同步等待和喚醒的屬性
std::condition_variable condition_;
// 解碼器真正進(jìn)行解碼的函數(shù)
void Decode(AVCodecContext* codec_ctx, AVPacket* pkt, AVFrame* frame, AVStream* stream,
std::unique_lock<std::mutex>& lock, SwsContext* sws_context, AVFrame* pFrame);
public:
// 新建解碼器時(shí)要傳入媒體文件路徑和一個(gè)解碼后的回調(diào) on_decode_frame雅宾。
VideoDecoder(const char* path, std::function<void(long timestamp)> on_decode_frame);
// 在 JNI 層將上層傳入的 Surface 包裝后新建一個(gè) ANativeWindow 傳入养涮,在后面解碼后繪制幀數(shù)據(jù)時(shí)需要用到
void Prepare(ANativeWindow* window);
// 抽取指定時(shí)間戳的視頻幀,可由上層調(diào)用
bool DecodeFrame(long time_ms);
// 釋放解碼器資源
void Release();
// 獲取當(dāng)前系統(tǒng)毫秒時(shí)間
static int64_t GetCurrentMilliTime(void);
};
}
VideoDecoder.cpp
#include "VideoDecoder.h"
#include "../log/Logger.h"
#include <thread>
#include <utility>
extern "C" {
#include <libavutil/imgutils.h>
}
#define TAG "VideoDecoder"
namespace codec {
VideoDecoder::VideoDecoder(const char* path, std::function<void(long timestamp)> on_decode_frame)
: on_decode_frame_(std::move(on_decode_frame)) {
path_ = std::string(path);
}
void VideoDecoder::Decode(AVCodecContext* codec_ctx, AVPacket* pkt, AVFrame* frame, AVStream* stream,
std::unique_lock<std::mutex>& lock, SwsContext* sws_context,
AVFrame* rgba_frame) {
int ret;
/* send the packet with the compressed data to the decoder */
ret = avcodec_send_packet(codec_ctx, pkt);
if (ret == AVERROR(EAGAIN)) {
LOGE(TAG,
"Decode: Receive_frame and send_packet both returned EAGAIN, which is an API violation.");
} else if (ret < 0) {
return;
}
// read all the output frames (infile general there may be any number of them
while (ret >= 0 && !is_stop_) {
// 對(duì)于frame, avcodec_receive_frame內(nèi)部每次都先調(diào)用
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return;
} else if (ret < 0) {
return;
}
int64_t startTime = GetCurrentMilliTime();
LOGD(TAG, "decodeStartTime: %ld", startTime);
// 換算當(dāng)前解碼的frame時(shí)間戳
auto decode_time_ms = frame->pts * 1000 / stream->time_base.den;
LOGD(TAG, "decode_time_ms = %ld", decode_time_ms);
if (decode_time_ms >= time_ms_) {
LOGD(TAG, "decode decode_time_ms = %ld, time_ms_ = %ld", decode_time_ms, time_ms_);
last_decode_time_ms_ = decode_time_ms;
is_seeking_ = false;
// 數(shù)據(jù)格式轉(zhuǎn)換
int result = sws_scale(
sws_context,
(const uint8_t* const*) frame->data, frame->linesize,
0, video_height_,
rgba_frame->data, rgba_frame->linesize);
if (result <= 0) {
LOGE(TAG, "Player Error : data convert fail");
return;
}
// 播放
result = ANativeWindow_lock(native_window_, &window_buffer_, nullptr);
if (result < 0) {
LOGE(TAG, "Player Error : Can not lock native window");
} else {
// 將圖像繪制到界面上
auto bits = (uint8_t*) window_buffer_.bits;
for (int h = 0; h < video_height_; h++) {
memcpy(bits + h * window_buffer_.stride * 4,
out_buffer_ + h * rgba_frame->linesize[0],
rgba_frame->linesize[0]);
}
ANativeWindow_unlockAndPost(native_window_);
}
on_decode_frame_(decode_time_ms);
int64_t endTime = GetCurrentMilliTime();
LOGD(TAG, "decodeEndTime - decodeStartTime: %ld", endTime - startTime);
LOGD(TAG, "finish decode frame");
condition_.wait(lock);
}
// 主要作用是清理AVPacket中的所有空間數(shù)據(jù)秀又,清理完畢后進(jìn)行初始化操作单寂,并且將 data 與 size 置為0,方便下次調(diào)用吐辙。
// 釋放 packet 引用
av_packet_unref(pkt);
}
}
void VideoDecoder::Prepare(ANativeWindow* window) {
native_window_ = window;
av_register_all();
auto av_format_context = avformat_alloc_context();
avformat_open_input(&av_format_context, path_.c_str(), nullptr, nullptr);
avformat_find_stream_info(av_format_context, nullptr);
int video_stream_index = -1;
for (int i = 0; i < av_format_context->nb_streams; i++) {
// 找到視頻媒體流的下標(biāo)
if (av_format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_index = i;
LOGD(TAG, "find video stream index = %d", video_stream_index);
break;
}
}
// run once
do {
if (video_stream_index == -1) {
codec::LOGE(TAG, "Player Error : Can not find video stream");
break;
}
std::unique_lock<std::mutex> lock(work_queue_mtx);
// 獲取視頻媒體流
auto stream = av_format_context->streams[video_stream_index];
// 找到已注冊(cè)的解碼器
auto codec = avcodec_find_decoder(stream->codecpar->codec_id);
// 獲取解碼器上下文
AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
auto ret = avcodec_parameters_to_context(codec_ctx, stream->codecpar);
if (ret >= 0) {
// 打開
avcodec_open2(codec_ctx, codec, nullptr);
// 解碼器打開后才有寬高的值
video_width_ = codec_ctx->width;
video_height_ = codec_ctx->height;
AVFrame* rgba_frame = av_frame_alloc();
int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGBA, video_width_, video_height_,
1);
// 分配內(nèi)存空間給輸出 buffer
out_buffer_ = (uint8_t*) av_malloc(buffer_size * sizeof(uint8_t));
av_image_fill_arrays(rgba_frame->data, rgba_frame->linesize, out_buffer_,
AV_PIX_FMT_RGBA,
video_width_, video_height_, 1);
// 通過設(shè)置寬高限制緩沖區(qū)中的像素?cái)?shù)量宣决,而非屏幕的物理顯示尺寸。
// 如果緩沖區(qū)與物理屏幕的顯示尺寸不相符昏苏,則實(shí)際顯示可能會(huì)是拉伸尊沸,或者被壓縮的圖像
int result = ANativeWindow_setBuffersGeometry(native_window_, video_width_,
video_height_, WINDOW_FORMAT_RGBA_8888);
if (result < 0) {
LOGE(TAG, "Player Error : Can not set native window buffer");
avcodec_close(codec_ctx);
avcodec_free_context(&codec_ctx);
av_free(out_buffer_);
break;
}
auto frame = av_frame_alloc();
auto packet = av_packet_alloc();
struct SwsContext* data_convert_context = sws_getContext(
video_width_, video_height_, codec_ctx->pix_fmt,
video_width_, video_height_, AV_PIX_FMT_RGBA,
SWS_BICUBIC, nullptr, nullptr, nullptr);
while (!is_stop_) {
LOGD(TAG, "front seek time_ms_ = %ld, last_decode_time_ms_ = %ld", time_ms_,
last_decode_time_ms_);
if (!is_seeking_ && (time_ms_ > last_decode_time_ms_ + 1000 ||
time_ms_ < last_decode_time_ms_ - 50)) {
is_seeking_ = true;
LOGD(TAG, "seek frame time_ms_ = %ld, last_decode_time_ms_ = %ld", time_ms_,
last_decode_time_ms_);
// 傳進(jìn)去的是指定幀帶有 time_base 的時(shí)間戳贤惯,所以是要將原來的 times_ms 按照上面獲取時(shí)的計(jì)算方式反推算出時(shí)間戳
av_seek_frame(av_format_context, video_stream_index,
time_ms_ * stream->time_base.den / 1000, AVSEEK_FLAG_BACKWARD);
}
// 讀取視頻一幀(完整的一幀)洼专,獲取的是一幀視頻的壓縮數(shù)據(jù),接下來才能對(duì)其進(jìn)行解碼
ret = av_read_frame(av_format_context, packet);
if (ret < 0) {
avcodec_flush_buffers(codec_ctx);
av_seek_frame(av_format_context, video_stream_index,
time_ms_ * stream->time_base.den / 1000, AVSEEK_FLAG_BACKWARD);
LOGD(TAG, "ret < 0, condition_.wait(lock)");
// 防止解碼最后一幀時(shí)視頻已經(jīng)沒有數(shù)據(jù)
on_decode_frame_(last_decode_time_ms_);
condition_.wait(lock);
}
if (packet->size) {
Decode(codec_ctx, packet, frame, stream, lock, data_convert_context,
rgba_frame);
}
}
// 釋放資源
sws_freeContext(data_convert_context);
av_free(out_buffer_);
av_frame_free(&rgba_frame);
av_frame_free(&frame);
av_packet_free(&packet);
}
avcodec_close(codec_ctx);
avcodec_free_context(&codec_ctx);
} while (false);
avformat_close_input(&av_format_context);
avformat_free_context(av_format_context);
ANativeWindow_release(native_window_);
delete this;
}
bool VideoDecoder::DecodeFrame(long time_ms) {
LOGD(TAG, "DecodeFrame time_ms = %ld", time_ms);
if (last_decode_time_ms_ == time_ms || time_ms_ == time_ms) {
LOGD(TAG, "DecodeFrame last_decode_time_ms_ == time_ms");
return false;
}
if (last_decode_time_ms_ >= time_ms && last_decode_time_ms_ <= time_ms + 50) {
return false;
}
time_ms_ = time_ms;
condition_.notify_all();
return true;
}
void VideoDecoder::Release() {
is_stop_ = true;
condition_.notify_all();
}
/**
* 獲取當(dāng)前的毫秒級(jí)時(shí)間
*/
int64_t VideoDecoder::GetCurrentMilliTime(void) {
struct timeval tv{};
gettimeofday(&tv, nullptr);
return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0;
}
}