当前位置: 首页 > news >正文

音视频编解码全流程之用Extractor后Decodec

  系列文章:

        音视频编解码全流程之提取器Extractor

        音视频编解码全流程之复用器Muxer

        音视频编解码全流程之用Extractor后Muxer生成MP4


根据前面叙述的 音视频编解码全流程之Extractor 可知:

媒体文件提取数据包的初始化及提取流程;

依此为基础现在本篇文章中实现“从媒体文件中Extractor提取数据包后,对提取后的数据进行解码的操作”。

一 .FFmpeg中提取媒体文件数据包后,对进行数据包解码:

        1.FFmpeg交叉编译:


        首先的是FFmpeg交叉编译,在之前的博客中有介绍交叉编译的全过程,感兴趣的可以查看博客:

         Android Liunx ffmpeg交叉编译

         macOs上交叉编译ffmpeg及安装ffmpeg工具

        2.提取媒体文件数据包:

        具体的流程细节分解在 音视频编解码全流程之复用器Muxer 已经进行了描述,这里不再赘述。

        

        3.查找解码器AVCodec和分配解码器实例AVCodecContext:

               ——> 这里是从媒体文件中封装实例AVFormatContext,查找音视频文件中的流信息 avformat_find_stream_info(in_fmt_ctx, nullptr)

                ——> 从封装实例中查找视频流的索引video_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);

                ——> 查找视频流AVStream,src_video = in_fmt_ctx->streams[video_index]。

                ——> 根据编解码器ID,查找解码器AVCodec *video_codec = (AVCodec *) avcodec_find_decoder(video_codec_id);

        以下是查找解码器AVCodec的函数代码:

// 打开输入文件
int RecodecVideo::open_input_file(const char *src_name) {// 打开音视频文件int ret = avformat_open_input(&in_fmt_ctx, src_name, nullptr, nullptr);if (ret < 0) {LOGE("Can't open file %s.\n", src_name);recodecInfo = "\n Can't open file :" + string(src_name);PostRecodecStatusMessage(recodecInfo.c_str());return -1;}LOGI("Success open input_file %s.\n", src_name);recodecInfo = "\n Success open input_file:" + string(src_name);PostRecodecStatusMessage(recodecInfo.c_str());// 查找音视频文件中的流信息ret = avformat_find_stream_info(in_fmt_ctx, nullptr);if (ret < 0) {LOGE("Can't find stream information.\n");av_strerror(ret, errbuf, sizeof(errbuf)); // 将错误码转换为字符串recodecInfo = "Can't find stream information:" + to_string(ret) + "\n error msg:" +string(errbuf) + "\n";PostRecodecStatusMessage(recodecInfo.c_str());return -1;}// 找到视频流的索引video_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);if (video_index >= 0) {src_video = in_fmt_ctx->streams[video_index];enum AVCodecID video_codec_id = src_video->codecpar->codec_id;// 查找视频解码器AVCodec *video_codec = (AVCodec *) avcodec_find_decoder(video_codec_id);if (!video_codec) {LOGE("video_codec not found\n");recodecInfo = "\n video_codec not found. ";PostRecodecStatusMessage(recodecInfo.c_str());return -1;}video_decode_ctx = avcodec_alloc_context3(video_codec); // 分配解码器的实例if (!video_decode_ctx) {LOGE("video_decode_ctx is nullptr\n");recodecInfo = "\n video_decode_ctx is nullptr ";PostRecodecStatusMessage(recodecInfo.c_str());return -1;}// 把视频流中的编解码参数复制给解码器的实例avcodec_parameters_to_context(video_decode_ctx, src_video->codecpar);ret = avcodec_open2(video_decode_ctx, video_codec, nullptr); // 打开解码器的实例if (ret < 0) {LOGE("Can't open video_decode_ctx.\n");recodecInfo = "Can't open video_decode_ctx\n";PostRecodecStatusMessage(recodecInfo.c_str());return -1;}} else {LOGE("Can't find video stream.\n");recodecInfo = "\n Can't find video stream.";PostRecodecStatusMessage(recodecInfo.c_str());return -1;}// 找到音频流的索引audio_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);if (audio_index >= 0) {src_audio = in_fmt_ctx->streams[audio_index];}return 0;
}

        4.对数据包进行解码:

        由上的提取过程后从文件中提取出AVPacket,在FFmpeg中接收AVFrame后对其解码出原始的数据帧AVFrame。

        ——> 把未解压的数据包发给解码器实例avcodec_send_packet(video_decode_ctx, packet);

        ——> 从解码器实例获取还原后的数据帧avcodec_receive_frame(video_decode_ctx, frame);

        以下是把AVPacket发送给解码器解码后得到AVFrame的函数代码:


// 对视频帧重新编码
int RecodecVideo::recode_video(AVPacket *packet, AVFrame *frame) {// 把未解压的数据包发给解码器实例int ret = avcodec_send_packet(video_decode_ctx, packet);if (ret < 0) {LOGE("send packet occur error %d.\n", ret);av_strerror(ret, errbuf, sizeof(errbuf)); // 将错误码转换为字符串recodecInfo = "send packet occur error:" + to_string(ret) + "\n error msg:" +string(errbuf) + "\n";PostRecodecStatusMessage(recodecInfo.c_str());return ret;}while (1) {// 从解码器实例获取还原后的数据帧ret = avcodec_receive_frame(video_decode_ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {return (ret == AVERROR(EAGAIN)) ? 0 : 1;} else if (ret < 0) {LOGE("decode frame occur error %d.\n", ret);recodecInfo = "\n decode frame occur error :" + to_string(ret);PostRecodecStatusMessage(recodecInfo.c_str());break;}if (frame->pts == AV_NOPTS_VALUE) { // 对H.264裸流做特殊处理double interval = 1.0 / av_q2d(src_video->r_frame_rate);frame->pts = count * interval / av_q2d(src_video->time_base);count++;}output_video(frame); // 给视频帧编码,并写入压缩后的视频包}return ret;
}

        5.完整的编解码的代码:

        以上的代码放在本人的GitHub项目中:https://github.com/wangyongyao1989/FFmpegPractices

        中的RecodecVideo.cpp:

//
// Created by wangyao on 2025/8/17.
//#include "includes/RecodecVideo.h"RecodecVideo::RecodecVideo(JNIEnv *env, jobject thiz) {mEnv = env;env->GetJavaVM(&mJavaVm);mJavaObj = env->NewGlobalRef(thiz);
}RecodecVideo::~RecodecVideo() {if (in_fmt_ctx) {in_fmt_ctx = nullptr;}if (video_encode_ctx) {video_encode_ctx = nullptr;}video_index = -1;audio_index = -1;if (src_video) {src_video = nullptr;}if (src_audio) {src_audio = nullptr;}if (dest_video) {dest_video = nullptr;}if (out_fmt_ctx) {out_fmt_ctx = nullptr;}if (video_encode_ctx) {video_encode_ctx = nullptr;}mEnv->DeleteGlobalRef(mJavaObj);if (mEnv) {mEnv = nullptr;}if (mJavaVm) {mJavaVm = nullptr;}if (mJavaObj) {mJavaObj = nullptr;}if (codecThread != nullptr) {codecThread->join();delete codecThread;codecThread = nullptr;}mSrcPath = nullptr;mDestPath = nullptr;}void RecodecVideo::startRecodecThread(const char *srcPath, const char *destPath) {mSrcPath = srcPath;mDestPath = destPath;if (open_input_file(mSrcPath) < 0) { // 打开输入文件return;}if (open_output_file(mDestPath) < 0) { // 打开输出文件return;}if (codecThread == nullptr) {codecThread = new thread(DoRecoding, this);codecThread->detach();}
}void RecodecVideo::DoRecoding(RecodecVideo *recodecVideo) {recodecVideo->recodecVideo();
}void RecodecVideo::recodecVideo() {int ret = -1;AVPacket *packet = av_packet_alloc(); // 分配一个数据包AVFrame *frame = av_frame_alloc(); // 分配一个数据帧while (av_read_frame(in_fmt_ctx, packet) >= 0) { // 轮询数据包if (packet->stream_index == video_index) { // 视频包需要重新编码packet->stream_index = 0;if (packet->buf->size < 600) {recodecInfo ="读出视频包的大小:" + to_string(packet->buf->size) + ",并重新编码写入...\n";PostRecodecStatusMessage(recodecInfo.c_str());}LOGD("%s.\n", recodecInfo.c_str());recode_video(packet, frame); // 对视频帧重新编码} else { // 音频包暂不重新编码,直接写入目标文件packet->stream_index = 1;ret = av_write_frame(out_fmt_ctx, packet); // 往文件写入一个数据包if (ret < 0) {LOGE("write frame occur error %d.\n", ret);av_strerror(ret, errbuf, sizeof(errbuf)); // 将错误码转换为字符串recodecInfo = "write frame occur error:" + to_string(ret) + "\n error msg:" +string(errbuf) + "\n";recodecInfo = "\n write frame occur error:" + to_string(ret);break;}}av_packet_unref(packet); // 清除数据包}packet->data = nullptr; // 传入一个空包,冲走解码缓存packet->size = 0;recode_video(packet, frame); // 对视频帧重新编码output_video(nullptr); // 传入一个空帧,冲走编码缓存av_write_trailer(out_fmt_ctx); // 写文件尾LOGI("Success recode file.\n");recodecInfo = "Success recode file!!!!!!\n\n";PostRecodecStatusMessage(recodecInfo.c_str());av_frame_free(&frame); // 释放数据帧资源av_packet_free(&packet); // 释放数据包资源avio_close(out_fmt_ctx->pb); // 关闭输出流avcodec_close(video_decode_ctx); // 关闭视频解码器的实例avcodec_free_context(&video_decode_ctx); // 释放视频解码器的实例avcodec_close(video_encode_ctx); // 关闭视频编码器的实例avcodec_free_context(&video_encode_ctx); // 释放视频编码器的实例avformat_free_context(out_fmt_ctx); // 释放封装器的实例avformat_close_input(&in_fmt_ctx); // 关闭音视频文件
}// 打开输入文件
int RecodecVideo::open_input_file(const char *src_name) {// 打开音视频文件int ret = avformat_open_input(&in_fmt_ctx, src_name, nullptr, nullptr);if (ret < 0) {LOGE("Can't open file %s.\n", src_name);recodecInfo = "\n Can't open file :" + string(src_name);PostRecodecStatusMessage(recodecInfo.c_str());return -1;}LOGI("Success open input_file %s.\n", src_name);recodecInfo = "\n Success open input_file:" + string(src_name);PostRecodecStatusMessage(recodecInfo.c_str());// 查找音视频文件中的流信息ret = avformat_find_stream_info(in_fmt_ctx, nullptr);if (ret < 0) {LOGE("Can't find stream information.\n");av_strerror(ret, errbuf, sizeof(errbuf)); // 将错误码转换为字符串recodecInfo = "Can't find stream information:" + to_string(ret) + "\n error msg:" +string(errbuf) + "\n";PostRecodecStatusMessage(recodecInfo.c_str());return -1;}// 找到视频流的索引video_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);if (video_index >= 0) {src_video = in_fmt_ctx->streams[video_index];enum AVCodecID video_codec_id = src_video->codecpar->codec_id;// 查找视频解码器AVCodec *video_codec = (AVCodec *) avcodec_find_decoder(video_codec_id);if (!video_codec) {LOGE("video_codec not found\n");recodecInfo = "\n video_codec not found. ";PostRecodecStatusMessage(recodecInfo.c_str());return -1;}video_decode_ctx = avcodec_alloc_context3(video_codec); // 分配解码器的实例if (!video_decode_ctx) {LOGE("video_decode_ctx is nullptr\n");recodecInfo = "\n video_decode_ctx is nullptr ";PostRecodecStatusMessage(recodecInfo.c_str());return -1;}// 把视频流中的编解码参数复制给解码器的实例avcodec_parameters_to_context(video_decode_ctx, src_video->codecpar);ret = avcodec_open2(video_decode_ctx, video_codec, nullptr); // 打开解码器的实例if (ret < 0) {LOGE("Can't open video_decode_ctx.\n");recodecInfo = "Can't open video_decode_ctx\n";PostRecodecStatusMessage(recodecInfo.c_str());return -1;}} else {LOGE("Can't find video stream.\n");recodecInfo = "\n Can't find video stream.";PostRecodecStatusMessage(recodecInfo.c_str());return -1;}// 找到音频流的索引audio_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);if (audio_index >= 0) {src_audio = in_fmt_ctx->streams[audio_index];}return 0;
}// 给视频帧编码,并写入压缩后的视频包
int RecodecVideo::output_video(AVFrame *frame) {// 把原始的数据帧发给编码器实例int ret = avcodec_send_frame(video_encode_ctx, frame);if (ret < 0) {LOGE("send frame occur error %d.\n", ret);av_strerror(ret, errbuf, sizeof(errbuf)); // 将错误码转换为字符串recodecInfo = "send frame occur error :" + to_string(ret) + "\n error msg:" +string(errbuf) + "\n";PostRecodecStatusMessage(recodecInfo.c_str());return ret;}while (1) {AVPacket *packet = av_packet_alloc(); // 分配一个数据包// 从编码器实例获取压缩后的数据包ret = avcodec_receive_packet(video_encode_ctx, packet);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {return (ret == AVERROR(EAGAIN)) ? 0 : 1;} else if (ret < 0) {LOGE("encode frame occur error %d.\n", ret);av_strerror(ret, errbuf, sizeof(errbuf)); // 将错误码转换为字符串recodecInfo = "encode frame occur error :" + to_string(ret) + "\n error msg:" +string(errbuf) + "\n";PostRecodecStatusMessage(recodecInfo.c_str());break;}// 把数据包的时间戳从一个时间基转换为另一个时间基av_packet_rescale_ts(packet, src_video->time_base, dest_video->time_base);
//        LOGI( "pts=%ld, dts=%ld.\n", packet->pts, packet->dts);packet->stream_index = 0;ret = av_write_frame(out_fmt_ctx, packet); // 往文件写入一个数据包if (ret < 0) {LOGE("write frame occur error %d.\n", ret);av_strerror(ret, errbuf, sizeof(errbuf)); // 将错误码转换为字符串recodecInfo = "write frame occur error:" + to_string(ret) + "\n error msg:" +string(errbuf) + "\n";PostRecodecStatusMessage(recodecInfo.c_str());break;}av_packet_unref(packet); // 清除数据包}return ret;
}// 对视频帧重新编码
int RecodecVideo::recode_video(AVPacket *packet, AVFrame *frame) {// 把未解压的数据包发给解码器实例int ret = avcodec_send_packet(video_decode_ctx, packet);if (ret < 0) {LOGE("send packet occur error %d.\n", ret);av_strerror(ret, errbuf, sizeof(errbuf)); // 将错误码转换为字符串recodecInfo = "send packet occur error:" + to_string(ret) + "\n error msg:" +string(errbuf) + "\n";PostRecodecStatusMessage(recodecInfo.c_str());return ret;}while (1) {// 从解码器实例获取还原后的数据帧ret = avcodec_receive_frame(video_decode_ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {return (ret == AVERROR(EAGAIN)) ? 0 : 1;} else if (ret < 0) {LOGE("decode frame occur error %d.\n", ret);recodecInfo = "\n decode frame occur error :" + to_string(ret);PostRecodecStatusMessage(recodecInfo.c_str());break;}if (frame->pts == AV_NOPTS_VALUE) { // 对H.264裸流做特殊处理double interval = 1.0 / av_q2d(src_video->r_frame_rate);frame->pts = count * interval / av_q2d(src_video->time_base);count++;}output_video(frame); // 给视频帧编码,并写入压缩后的视频包}return ret;
}int RecodecVideo::open_output_file(const char *dest_name) {// 分配音视频文件的封装实例int ret = avformat_alloc_output_context2(&out_fmt_ctx, nullptr, nullptr, dest_name);if (ret < 0) {LOGE("Can't alloc output_file %s.\n", dest_name);recodecInfo = "\n Can't alloc output_file :" + string(dest_name);PostRecodecStatusMessage(recodecInfo.c_str());return -1;}// 打开输出流ret = avio_open(&out_fmt_ctx->pb, dest_name, AVIO_FLAG_READ_WRITE);if (ret < 0) {LOGE("Can't open output_file %s.\n", dest_name);recodecInfo = "\n Can't open output_file:" + string(dest_name);PostRecodecStatusMessage(recodecInfo.c_str());return -1;}LOGI("Success open output_file %s.\n", dest_name);recodecInfo = "\n Success open output_file :" + string(dest_name);PostRecodecStatusMessage(recodecInfo.c_str());if (video_index >= 0) { // 创建编码器实例和新的视频流enum AVCodecID video_codec_id = src_video->codecpar->codec_id;// 查找视频编码器
//        AVCodec *video_codec = (AVCodec *) avcodec_find_encoder(video_codec_id);//使用libx264的编码器AVCodec *video_codec = (AVCodec *) avcodec_find_encoder_by_name("libx264");if (!video_codec) {LOGE("video_codec not found\n");recodecInfo = "\n video_codec not found .";PostRecodecStatusMessage(recodecInfo.c_str());return -1;}video_encode_ctx = avcodec_alloc_context3(video_codec); // 分配编码器的实例if (!video_encode_ctx) {LOGE("video_encode_ctx is null\n");recodecInfo = "\n video_encode_ctx is null";PostRecodecStatusMessage(recodecInfo.c_str());return -1;}// 把源视频流中的编解码参数复制给编码器的实例avcodec_parameters_to_context(video_encode_ctx, src_video->codecpar);// 注意:帧率和时间基要单独赋值,因为avcodec_parameters_to_context没复制这两个参数video_encode_ctx->framerate = src_video->r_frame_rate;// framerate.num值过大,会导致视频头一秒变灰色if (video_encode_ctx->framerate.num > 60) {video_encode_ctx->framerate = (AVRational) {25, 1}; // 帧率}video_encode_ctx->time_base = src_video->time_base;video_encode_ctx->gop_size = 12; // 关键帧的间隔距离//video_encode_ctx->max_b_frames = 0; // 0表示不要B帧// AV_CODEC_FLAG_GLOBAL_HEADER标志允许操作系统显示该视频的缩略图if (out_fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) {video_encode_ctx->flags = AV_CODEC_FLAG_GLOBAL_HEADER;}ret = avcodec_open2(video_encode_ctx, video_codec, nullptr); // 打开编码器的实例if (ret < 0) {LOGE("Can't open video_encode_ctx.\n");av_strerror(ret, errbuf, sizeof(errbuf)); // 将错误码转换为字符串LOGE("avcodec_open2失败:%s\n", errbuf);recodecInfo = "\n avcodec_open2失败:" + string(errbuf);PostRecodecStatusMessage(recodecInfo.c_str());return -1;}dest_video = avformat_new_stream(out_fmt_ctx, nullptr); // 创建数据流// 把编码器实例的参数复制给目标视频流avcodec_parameters_from_context(dest_video->codecpar, video_encode_ctx);// 如果后面有对视频帧转换时间基,这里就无需复制时间基//dest_video->time_base = src_video->time_base;dest_video->codecpar->codec_tag = 0;}if (audio_index >= 0) { // 源文件有音频流,就给目标文件创建音频流AVStream *dest_audio = avformat_new_stream(out_fmt_ctx, nullptr); // 创建数据流// 把源文件的音频参数原样复制过来avcodec_parameters_copy(dest_audio->codecpar, src_audio->codecpar);dest_audio->codecpar->codec_tag = 0;}ret = avformat_write_header(out_fmt_ctx, nullptr); // 写文件头if (ret < 0) {LOGE("write file_header occur error %d.\n", ret);recodecInfo = "\n write file_header occur error :" + to_string(ret);av_strerror(ret, errbuf, sizeof(errbuf)); // 将错误码转换为字符串recodecInfo = "\n avformat_write_header 失败:" + string(errbuf);PostRecodecStatusMessage(recodecInfo.c_str());return -1;}LOGI("Success write file_header.\n");recodecInfo = "Success write file_header.\n";PostRecodecStatusMessage(recodecInfo.c_str());return 0;
}JNIEnv *RecodecVideo::GetJNIEnv(bool *isAttach) {JNIEnv *env;int status;if (nullptr == mJavaVm) {LOGD("RecodecVideo::GetJNIEnv mJavaVm == nullptr");return nullptr;}*isAttach = false;status = mJavaVm->GetEnv((void **) &env, JNI_VERSION_1_6);if (status != JNI_OK) {status = mJavaVm->AttachCurrentThread(&env, nullptr);if (status != JNI_OK) {LOGD("RecodecVideo::GetJNIEnv failed to attach current thread");return nullptr;}*isAttach = true;}return env;
}void RecodecVideo::PostRecodecStatusMessage(const char *msg) {bool isAttach = false;JNIEnv *pEnv = GetJNIEnv(&isAttach);if (pEnv == nullptr) {return;}jobject javaObj = mJavaObj;jmethodID mid = pEnv->GetMethodID(pEnv->GetObjectClass(javaObj), "CppStatusCallback","(Ljava/lang/String;)V");jstring pJstring = pEnv->NewStringUTF(msg);pEnv->CallVoidMethod(javaObj, mid, pJstring);if (isAttach) {JavaVM *pJavaVm = mJavaVm;pJavaVm->DetachCurrentThread();}
}

二.MediaCodec中提取媒体文件数据包后,对进行数据包解码:

        硬件编解码在Android中必须用到MediaCodec提供的下层编解码芯片的接口,在NdkMediaCodecAMediaCodec_createDecoderByType方法来获取解码器。

        1.初始化Extractor:

        ——> 创建 AMediaExtractor_new()

        ——> 设置Extractor的fd AMediaExtractor_setDataSourceFd(extractor, input_fd, 0, fileSize)

bool MediaExtratorDecodec::initExtractor() {extractor = AMediaExtractor_new();if (!extractor) {LOGE("Failed to create media extractor ");callbackInfo ="Failed to create media extractor \n";PostStatusMessage(callbackInfo.c_str());return false;}LOGE("inputPath:%s", sSrcPath.c_str());FILE *inputFp = fopen(sSrcPath.c_str(), "rb");if (!inputFp) {LOGE("Unable to open output file :%s", sSrcPath.c_str());callbackInfo ="Unable to open output file :" + sSrcPath + "\n";PostStatusMessage(callbackInfo.c_str());return false;}struct stat buf;stat(sSrcPath.c_str(), &buf);size_t fileSize = buf.st_size;int32_t input_fd = fileno(inputFp);LOGE("input_fd:%d", input_fd);media_status_t status = AMediaExtractor_setDataSourceFd(extractor, input_fd, 0, fileSize);if (status != AMEDIA_OK) {LOGE("Failed to set data source: %d", status);callbackInfo ="Failed to set data source :" + to_string(status) + "\n";PostStatusMessage(callbackInfo.c_str());return false;}LOGI("Extractor initialized successfully");return true;
}

        2.选择轨道获取AMediaFormat:  

        ——>  AMediaExtractor_getTrackCount()获取取器Extractor中的轨道数

        ——> AMediaExtractor_getTrackFormat(extractor, i) 遍历轨道从中获取每个轨道中的AMediaFormat

        ——> 筛选出音频轨道和视频轨道,分别获取音频轨道的audioTrackIndex值和视频轨道videoTrackIndex值。

// 选择轨道获取AMediaFormat
bool MediaExtratorDecodec::selectTracksAndGetFormat() {LOGI("selectTracksAndGetFormat===========");size_t trackCount = AMediaExtractor_getTrackCount(extractor);LOGI("Total tracks: %zu", trackCount);callbackInfo ="Total tracks:" + to_string(trackCount) + "\n";PostStatusMessage(callbackInfo.c_str());for (size_t i = 0; i < trackCount; i++) {AMediaFormat *format = AMediaExtractor_getTrackFormat(extractor, i);if (!format) continue;const char *mime;if (AMediaFormat_getString(format, AMEDIAFORMAT_KEY_MIME, &mime)) {LOGI("Track %zu: MIME=%s", i, mime);if (strncmp(mime, "video/", 6) == 0 && videoTrackIndex == -1) {videoTrackIndex = i;hasVideo = true;// 获取视频格式信息AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_WIDTH, &videoWidth);AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_HEIGHT, &videoHeight);AMediaFormat_getInt64(format, AMEDIAFORMAT_KEY_DURATION, &videoDuration);LOGI("Selected video track: %d", videoTrackIndex);LOGI("Video track: %dx%d, duration: %lld us",videoWidth, videoHeight, videoDuration);callbackInfo ="Selected video track:" + to_string(videoTrackIndex) + "\n";callbackInfo = callbackInfo + ",videoWidth:" + to_string(videoWidth)+ ",videoHeight:" + to_string(videoHeight) + ",videoDuration:"+ to_string(videoDuration) + "\n";PostStatusMessage(callbackInfo.c_str());} else if (strncmp(mime, "audio/", 6) == 0 && audioTrackIndex == -1) {audioTrackIndex = i;hasAudio = true;// 获取音频格式信息AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_SAMPLE_RATE, &audioSampleRate);AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_CHANNEL_COUNT, &audioChannelCount);LOGI("Audio track: sampleRate=%d, channels=%d",audioSampleRate, audioChannelCount);LOGI("Selected audio track: %d", audioTrackIndex);callbackInfo ="Selected audio track:" + to_string(audioTrackIndex) + "\n";callbackInfo = callbackInfo + ",audioSampleRate:" + to_string(audioSampleRate)+ ",audioChannelCount:" + to_string(audioChannelCount) + "\n";PostStatusMessage(callbackInfo.c_str());}}AMediaFormat_delete(format);}return hasVideo || hasAudio;
}

        3.初始化解码器:

        ——> 切换Extractor音视频的轨道,AMediaExtractor_getTrackFormat(extractor, videoTrackIndex) 方法中获取AMediaFormat

        ——> 通过AMediaCodec_createDecoderByType创建AMediaCodec

,并设置编解码AMediaCodec_configure(codec, format, nullptr, nullptr, isEncoder)。

        以下是初始化编解码相关代码:

// 初始化编解码器
bool MediaExtratorDecodec::initDecodec(bool asyncMode) {// 添加视频轨道if (hasVideo) {AMediaExtractor_selectTrack(extractor, videoTrackIndex);mVideoFormat = AMediaExtractor_getTrackFormat(extractor, videoTrackIndex);AMediaFormat_getString(mVideoFormat, AMEDIAFORMAT_KEY_MIME, &video_mime);LOGI("video_mime: %s", video_mime);callbackInfo ="video_mime:" + string(video_mime) + "\n";PostStatusMessage(callbackInfo.c_str());mVideoCodec = createMediaCodec(mVideoFormat, video_mime, "", false /*isEncoder*/);if (!mVideoCodec) {LOGE("Failed to create video codec");callbackInfo ="Failed to create video codec \n";PostStatusMessage(callbackInfo.c_str());return false;}if (asyncMode) {AMediaCodecOnAsyncNotifyCallback aCB = {OnInputAvailableCB, OnOutputAvailableCB,OnFormatChangedCB, OnErrorCB};AMediaCodec_setAsyncNotifyCallback(mVideoCodec, aCB, this);ThreadTask task = []() {CallBackHandle();};g_threadManager->submitTask("video-decode-Thread", task, PRIORITY_NORMAL);}LOGI("create video codec success");callbackInfo ="create video codec success:  \n";PostStatusMessage(callbackInfo.c_str());AMediaCodec_start(mVideoCodec);}// 添加音频轨道if (hasAudio) {AMediaExtractor_selectTrack(extractor, audioTrackIndex);mAudioFormat = AMediaExtractor_getTrackFormat(extractor, audioTrackIndex);AMediaFormat_getString(mAudioFormat, AMEDIAFORMAT_KEY_MIME, &audio_mime);LOGI("audio_mime: %s", audio_mime);callbackInfo ="audio_mime:" + string(audio_mime) + "\n";PostStatusMessage(callbackInfo.c_str());mAudioCodec = createMediaCodec(mAudioFormat, audio_mime, "", false /*isEncoder*/);if (!mAudioCodec) {LOGE("Failed to create audio codec");callbackInfo ="Failed to create audio codec \n";PostStatusMessage(callbackInfo.c_str());return false;}if (asyncMode) {AMediaCodecOnAsyncNotifyCallback aCB = {OnInputAvailableCB, OnOutputAvailableCB,OnFormatChangedCB, OnErrorCB};AMediaCodec_setAsyncNotifyCallback(mAudioCodec, aCB, this);ThreadTask task = []() {CallBackHandle();};g_threadManager->submitTask("audio-decode-Thread", task, PRIORITY_NORMAL);}LOGI("create audio codec success");callbackInfo ="create audio codec success:  \n";PostStatusMessage(callbackInfo.c_str());AMediaCodec_start(mAudioCodec);}LOGI("initDecodec initialized successfully");callbackInfo ="initDecodec initialized successfully \n";PostStatusMessage(callbackInfo.c_str());return true;
}AMediaCodec *createMediaCodec(AMediaFormat *format, const char *mime, string codecName,bool isEncoder) {ALOGV("In %s", __func__);if (!mime) {ALOGE("Please specify a mime type to create codec");return nullptr;}AMediaCodec *codec;if (!codecName.empty()) {codec = AMediaCodec_createCodecByName(codecName.c_str());if (!codec) {ALOGE("Unable to create codec by name: %s", codecName.c_str());return nullptr;}ALOGV("create codec by name: %s", codecName.c_str());} else {if (isEncoder) {codec = AMediaCodec_createEncoderByType(mime);} else {codec = AMediaCodec_createDecoderByType(mime);}if (!codec) {ALOGE("Unable to create codec by mime: %s", mime);return nullptr;}char *out_name = nullptr;AMediaCodec_getName(codec, &out_name);ALOGV("create codec by mime: %s", out_name);}/* Configure codec with the given format*/const char *s = AMediaFormat_toString(format);ALOGI("Input format: %s\n", s);media_status_t status = AMediaCodec_configure(codec, format, nullptr, nullptr, isEncoder);if (status != AMEDIA_OK) {ALOGE("AMediaCodec_configure failed %d", status);return nullptr;}return codec;
}

        4.解码过程的操作:

        解码过程解析:

——> 重新选择所有轨道以重置读取位置 AMediaExtractor_selectTrack(extractor, videoTrackIndex) 和 AMediaExtractor_selectTrack(extractor, audioTrackIndex);

——> 设置读取位置到开始 AMediaExtractor_seekTo(extractor, 0,         AMEDIAEXTRACTOR_SEEK_CLOSEST_SYNC);

        ——> 遍历 获取下一个可用输入缓冲区的索引

ssize_t inIdx = AMediaCodec_dequeueInputBuffer(mVideoCodec,kQueueDequeueTimeoutUs)

        根据inIdx的状态值得到可用的输入:

        onInputAvailable(mVideoCodec, inIdx);

                ——> 从提取器Extractor中获取到该样本的大小:

        size_t bufSize = AMediaExtractor_getSampleSize(extractor);

                ——> 获取输入缓冲区,以备输入的样本数据的填充:

        uint8_t *buf = AMediaCodec_getInputBuffer(mVideoCodec, bufIdx, &bufSize);

                ——> 从提取器读取数据包,填充至输入缓冲区中:

        ssize_t bytesRead = AMediaExtractor_readSampleData(extractor, buf, bufSize);

                ——> 把缓冲区的buf送入解码器:

        media_status_t status = AMediaCodec_queueInputBuffer(mVideoCodec, bufIdx, 0 /* offset */,bytesRead, presentationTimeUs, flag);

        —— 遍历   获取下一个可用已处理数据缓冲区的索引:

        ssize_t outIdx = AMediaCodec_dequeueOutputBuffer(mAudioCodec, &info,kQueueDequeueTimeoutUs);

              根据outIdx的状态值得到可用的输入:

        onOutputAvailable(mVideoCodec, outIdx, &info);

               ——> 获取输出缓冲区的数据:

        uint8_t *buf = AMediaCodec_getOutputBuffer(mVideoCodec, bufIdx, &bufSize);

                decodec():

        选择对应的音视频轨道,从提取器中提取数据包进行解码的操作。

        以下是解码的代码:

// 执行解码
bool MediaExtratorDecodec::decodec() {LOGI("decodec===========");bool asyncMode = false;AMediaCodecBufferInfo info;bool sawEOS = false;int64_t lastVideoPts = -1;int64_t lastAudioPts = -1;// 重新选择所有轨道以重置读取位置if (hasVideo) AMediaExtractor_selectTrack(extractor, videoTrackIndex);if (hasAudio) AMediaExtractor_selectTrack(extractor, audioTrackIndex);// 设置读取位置到开始AMediaExtractor_seekTo(extractor, 0, AMEDIAEXTRACTOR_SEEK_CLOSEST_SYNC);while (!sawEOS) {ssize_t trackIndex = AMediaExtractor_getSampleTrackIndex(extractor);if (trackIndex < 0) {sawEOS = true;break;}if (trackIndex == videoTrackIndex && hasVideo) {// 检查时间戳是否有效(避免重复或倒退的时间戳)if (AMediaExtractor_getSampleTime(extractor) > lastVideoPts) {if (!asyncMode) {while (!mSawOutputEOS && !mSignalledError) {/* Queue input data */if (!mSawInputEOS) {ssize_t inIdx = AMediaCodec_dequeueInputBuffer(mVideoCodec,kQueueDequeueTimeoutUs);if (inIdx < 0 && inIdx != AMEDIACODEC_INFO_TRY_AGAIN_LATER) {LOGE("AMediaCodec_dequeueInputBuffer returned invalid index %zd\n",inIdx);mErrorCode = (media_status_t) inIdx;return mErrorCode;} else if (inIdx >= 0) {onInputAvailable(mVideoCodec, inIdx);}}/* Dequeue output data */AMediaCodecBufferInfo info;ssize_t outIdx = AMediaCodec_dequeueOutputBuffer(mVideoCodec, &info,kQueueDequeueTimeoutUs);if (outIdx == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {mVideoFormat = AMediaCodec_getOutputFormat(mVideoCodec);const char *s = AMediaFormat_toString(mVideoFormat);LOGI("Output format: %s\n", s);} else if (outIdx >= 0) {onOutputAvailable(mVideoCodec, outIdx, &info);} else if (!(outIdx == AMEDIACODEC_INFO_TRY_AGAIN_LATER ||outIdx == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED)) {LOGE("AMediaCodec_dequeueOutputBuffer returned invalid index %zd\n",outIdx);mErrorCode = (media_status_t) outIdx;return mErrorCode;}}} else {unique_lock<mutex> lock(mMutex);mDecoderDoneCondition.wait(lock, [this]() {return (mSawOutputEOS || mSignalledError);});}if (mSignalledError) {LOGE("Received Error while Decoding");return mErrorCode;}lastVideoPts = info.presentationTimeUs;}} else if (trackIndex == audioTrackIndex && hasAudio) {     //音频轨道的解码// 检查时间戳是否有效if (info.presentationTimeUs > lastAudioPts) {// 检查时间戳是否有效(避免重复或倒退的时间戳)if (!asyncMode) {while (!mSawOutputEOS && !mSignalledError) {/* Queue input data */if (!mSawInputEOS) {ssize_t inIdx = AMediaCodec_dequeueInputBuffer(mAudioCodec,kQueueDequeueTimeoutUs);if (inIdx < 0 && inIdx != AMEDIACODEC_INFO_TRY_AGAIN_LATER) {LOGE("AMediaCodec_dequeueInputBuffer returned invalid index %zd\n",inIdx);mErrorCode = (media_status_t) inIdx;return mErrorCode;} else if (inIdx >= 0) {onInputAvailable(mAudioCodec, inIdx);}}/* Dequeue output data */AMediaCodecBufferInfo info;ssize_t outIdx = AMediaCodec_dequeueOutputBuffer(mAudioCodec, &info,kQueueDequeueTimeoutUs);if (outIdx == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {mAudioFormat = AMediaCodec_getOutputFormat(mAudioCodec);const char *s = AMediaFormat_toString(mAudioFormat);LOGI("Output format: %s\n", s);} else if (outIdx >= 0) {onOutputAvailable(mVideoCodec, outIdx, &info);} else if (!(outIdx == AMEDIACODEC_INFO_TRY_AGAIN_LATER ||outIdx == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED)) {LOGE("AMediaCodec_dequeueOutputBuffer returned invalid index %zd\n",outIdx);mErrorCode = (media_status_t) outIdx;return mErrorCode;}}} else {unique_lock<mutex> lock(mMutex);mDecoderDoneCondition.wait(lock, [this]() {return (mSawOutputEOS || mSignalledError);});}if (mSignalledError) {ALOGE("Received Error while Decoding");return mErrorCode;}lastAudioPts = info.presentationTimeUs;}}// 短暂休眠以避免过度占用CPUstd::this_thread::sleep_for(std::chrono::milliseconds(1));}LOGI("media decodec completed");callbackInfo ="media decodec completed \n";PostStatusMessage(callbackInfo.c_str());return true;
}

        onInputAvailable():

        可用输入时,对数据的操作:

void MediaExtratorDecodec::onInputAvailable(AMediaCodec *mediaCodec, int32_t bufIdx) {LOGD("onInputAvailable %s", __func__);if (mediaCodec == mVideoCodec && mediaCodec) {if (mSawInputEOS || bufIdx < 0) return;if (mSignalledError) {CallBackHandle::mSawError = true;mDecoderDoneCondition.notify_one();return;}size_t bufSize = AMediaExtractor_getSampleSize(extractor);if (bufSize <= 0) {LOGE("AMediaExtractor_getSampleSize====");return;}// 获取输入缓冲区uint8_t *buf = AMediaCodec_getInputBuffer(mVideoCodec, bufIdx, &bufSize);if (!buf) {mErrorCode = AMEDIA_ERROR_IO;mSignalledError = true;mDecoderDoneCondition.notify_one();return;}// 从提取器读取数据ssize_t bytesRead = AMediaExtractor_readSampleData(extractor, buf, bufSize);if (bytesRead < 0) {LOGI("reading video sample data: %zd", bytesRead);// 输入结束AMediaCodec_queueInputBuffer(mVideoCodec, bufIdx, 0, 0, 0,AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM);LOGI("从提取器读取数据到结束尾");callbackInfo ="视频轨道从提取器读取数据到结束尾 reading sample data:" + to_string(bytesRead) +"\n";PostStatusMessage(callbackInfo.c_str());return;}uint32_t flag = AMediaExtractor_getSampleFlags(extractor);int64_t presentationTimeUs = AMediaExtractor_getSampleTime(extractor);if (flag == AMEDIA_ERROR_MALFORMED) {mErrorCode = (media_status_t) flag;mSignalledError = true;mDecoderDoneCondition.notify_one();return;}if (flag == AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) mSawInputEOS = true;LOGD("video - %s bytesRead : %zd presentationTimeUs : %" PRId64 " mSawInputEOS : %s",__FUNCTION__,bytesRead, presentationTimeUs, mSawInputEOS ? "TRUE" : "FALSE");// 将数据送入解码器media_status_t status = AMediaCodec_queueInputBuffer(mVideoCodec, bufIdx, 0 /* offset */,bytesRead, presentationTimeUs, flag);if (AMEDIA_OK != status) {mErrorCode = status;mSignalledError = true;mDecoderDoneCondition.notify_one();return;}if (!AMediaExtractor_advance(extractor)) {return;}} else if (mediaCodec == mAudioCodec && mediaCodec) {if (mSawInputEOS || bufIdx < 0) return;if (mSignalledError) {CallBackHandle::mSawError = true;mDecoderDoneCondition.notify_one();return;}size_t bufSize = AMediaExtractor_getSampleSize(extractor);if (bufSize <= 0) {LOGE("AMediaExtractor_getSampleSize====");return;}// 获取输入缓冲区uint8_t *buf = AMediaCodec_getInputBuffer(mAudioCodec, bufIdx, &bufSize);if (!buf) {mErrorCode = AMEDIA_ERROR_IO;mSignalledError = true;mDecoderDoneCondition.notify_one();return;}// 从提取器读取数据ssize_t bytesRead = AMediaExtractor_readSampleData(extractor, buf, bufSize);if (bytesRead < 0) {LOGI("reading audio sample data: %zd", bytesRead);// 输入结束AMediaCodec_queueInputBuffer(mAudioCodec, bufIdx, 0, 0, 0,AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM);LOGI("从提取器读取音频轨道数据到结束尾");callbackInfo ="音频轨道从提取器读取数据到结束尾 reading sample data:" + to_string(bytesRead) +"\n";PostStatusMessage(callbackInfo.c_str());return;}uint32_t flag = AMediaExtractor_getSampleFlags(extractor);int64_t presentationTimeUs = AMediaExtractor_getSampleTime(extractor);if (flag == AMEDIA_ERROR_MALFORMED) {mErrorCode = (media_status_t) flag;mSignalledError = true;mDecoderDoneCondition.notify_one();return;}if (flag == AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) mSawInputEOS = true;LOGD("audio - %s bytesRead : %zd presentationTimeUs : %" PRId64 " mSawInputEOS : %s",__FUNCTION__,bytesRead, presentationTimeUs, mSawInputEOS ? "TRUE" : "FALSE");// 将数据送入解码器media_status_t status = AMediaCodec_queueInputBuffer(mAudioCodec, bufIdx, 0 /* offset */,bytesRead, presentationTimeUs, flag);if (AMEDIA_OK != status) {mErrorCode = status;mSignalledError = true;mDecoderDoneCondition.notify_one();return;}if (!AMediaExtractor_advance(extractor)) {return;}}}

        onOutputAvailable():

        当有可用的输出,对数据的操作:

void MediaExtratorDecodec::onOutputAvailable(AMediaCodec *mediaCodec, int32_t bufIdx,AMediaCodecBufferInfo *bufferInfo) {LOGD("In %s", __func__);if (mediaCodec == mVideoCodec && mediaCodec) {if (mSawOutputEOS || bufIdx < 0) return;if (mSignalledError) {CallBackHandle::mSawError = true;mDecoderDoneCondition.notify_one();return;}if (mOutFp != nullptr) {size_t bufSize;uint8_t *buf = AMediaCodec_getOutputBuffer(mVideoCodec, bufIdx, &bufSize);if (buf) {fwrite(buf, sizeof(char), bufferInfo->size, mOutFp);LOGD("bytes written into file  %d\n", bufferInfo->size);}}AMediaCodec_releaseOutputBuffer(mVideoCodec, bufIdx, false);mSawOutputEOS = (0 != (bufferInfo->flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM));mNumOutputVideoFrame++;LOGD("video - %s index : %d  mSawOutputEOS : %s count : %u", __FUNCTION__, bufIdx,mSawOutputEOS ? "TRUE" : "FALSE", mNumOutputVideoFrame);if (mSawOutputEOS) {CallBackHandle::mIsDone = true;mDecoderDoneCondition.notify_one();}} else if (mediaCodec == mAudioCodec && mediaCodec) {if (mSawOutputEOS || bufIdx < 0) return;if (mSignalledError) {CallBackHandle::mSawError = true;mDecoderDoneCondition.notify_one();return;}if (mOutFp != nullptr) {size_t bufSize;uint8_t *buf = AMediaCodec_getOutputBuffer(mAudioCodec, bufIdx, &bufSize);if (buf) {fwrite(buf, sizeof(char), bufferInfo->size, mOutFp);LOGD("bytes written into file  %d\n", bufferInfo->size);}}AMediaCodec_releaseOutputBuffer(mAudioCodec, bufIdx, false);mSawOutputEOS = (0 != (bufferInfo->flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM));mNumOutputAudioFrame++;LOGD("video - %s index : %d  mSawOutputEOS : %s count : %u", __FUNCTION__, bufIdx,mSawOutputEOS ? "TRUE" : "FALSE", mNumOutputAudioFrame);if (mSawOutputEOS) {CallBackHandle::mIsDone = true;mDecoderDoneCondition.notify_one();}}
}

       5.完整的编解码的代码:

       以上的代码放在本人的GitHub项目中:https://github.com/wangyongyao1989/FFmpegPractices

        中的MediaExtratorDecodec.cpp:

//  Author : wangyongyao https://github.com/wangyongyao1989
// Created by MMM on 2025/9/29.
//#include <sys/stat.h>
#include "includes/MediaExtratorDecodec.h"MediaExtratorDecodec::MediaExtratorDecodec(JNIEnv *env, jobject thiz) {mEnv = env;env->GetJavaVM(&mJavaVm);mJavaObj = env->NewGlobalRef(thiz);// 初始化线程池g_threadManager = std::make_unique<AndroidThreadManager>();ThreadPoolConfig config;config.minThreads = 2;config.maxThreads = 4;config.idleTimeoutMs = 30000;config.queueSize = 50;g_threadManager->initThreadPool(config);}MediaExtratorDecodec::~MediaExtratorDecodec() {mEnv->DeleteGlobalRef(mJavaObj);if (mEnv) {mEnv = nullptr;}if (mJavaVm) {mJavaVm = nullptr;}if (mJavaObj) {mJavaObj = nullptr;}release();g_threadManager.reset();
}void
MediaExtratorDecodec::startMediaExtratorDecodec(const char *inputPath) {sSrcPath = inputPath;LOGI("sSrcPath :%s \n ", sSrcPath.c_str());callbackInfo ="sSrcPath:" + sSrcPath + "\n";PostStatusMessage(callbackInfo.c_str());// 1. 初始化提取器if (!initExtractor()) {LOGE("Failed to initialize extractor");callbackInfo ="Failed to initialize extractor \n";PostStatusMessage(callbackInfo.c_str());return;}// 2. 选择轨道if (!selectTracksAndGetFormat()) {LOGE("No valid tracks found");callbackInfo ="No valid tracks found \n";PostStatusMessage(callbackInfo.c_str());return;}// 3. 初始化解码器if (!initDecodec(false)) {LOGE("Failed to initialize Decodec");callbackInfo ="Failed to initialize Decodec \n";PostStatusMessage(callbackInfo.c_str());return;}// 4. 执行解码if (!decodec()) {LOGE("Decodec failed");callbackInfo ="Decodec failed \n";PostStatusMessage(callbackInfo.c_str());return;}// 释放资源release();
}// 初始化提取器
bool MediaExtratorDecodec::initExtractor() {extractor = AMediaExtractor_new();if (!extractor) {LOGE("Failed to create media extractor ");callbackInfo ="Failed to create media extractor \n";PostStatusMessage(callbackInfo.c_str());return false;}LOGE("inputPath:%s", sSrcPath.c_str());FILE *inputFp = fopen(sSrcPath.c_str(), "rb");if (!inputFp) {LOGE("Unable to open output file :%s", sSrcPath.c_str());callbackInfo ="Unable to open output file :" + sSrcPath + "\n";PostStatusMessage(callbackInfo.c_str());return false;}struct stat buf;stat(sSrcPath.c_str(), &buf);size_t fileSize = buf.st_size;int32_t input_fd = fileno(inputFp);LOGE("input_fd:%d", input_fd);media_status_t status = AMediaExtractor_setDataSourceFd(extractor, input_fd, 0, fileSize);if (status != AMEDIA_OK) {LOGE("Failed to set data source: %d", status);callbackInfo ="Failed to set data source :" + to_string(status) + "\n";PostStatusMessage(callbackInfo.c_str());return false;}LOGI("Extractor initialized successfully");return true;
}// 选择轨道获取AMediaFormat
bool MediaExtratorDecodec::selectTracksAndGetFormat() {LOGI("selectTracksAndGetFormat===========");size_t trackCount = AMediaExtractor_getTrackCount(extractor);LOGI("Total tracks: %zu", trackCount);callbackInfo ="Total tracks:" + to_string(trackCount) + "\n";PostStatusMessage(callbackInfo.c_str());for (size_t i = 0; i < trackCount; i++) {AMediaFormat *format = AMediaExtractor_getTrackFormat(extractor, i);if (!format) continue;const char *mime;if (AMediaFormat_getString(format, AMEDIAFORMAT_KEY_MIME, &mime)) {LOGI("Track %zu: MIME=%s", i, mime);if (strncmp(mime, "video/", 6) == 0 && videoTrackIndex == -1) {videoTrackIndex = i;hasVideo = true;// 获取视频格式信息AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_WIDTH, &videoWidth);AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_HEIGHT, &videoHeight);AMediaFormat_getInt64(format, AMEDIAFORMAT_KEY_DURATION, &videoDuration);LOGI("Selected video track: %d", videoTrackIndex);LOGI("Video track: %dx%d, duration: %lld us",videoWidth, videoHeight, videoDuration);callbackInfo ="Selected video track:" + to_string(videoTrackIndex) + "\n";callbackInfo = callbackInfo + ",videoWidth:" + to_string(videoWidth)+ ",videoHeight:" + to_string(videoHeight) + ",videoDuration:"+ to_string(videoDuration) + "\n";PostStatusMessage(callbackInfo.c_str());} else if (strncmp(mime, "audio/", 6) == 0 && audioTrackIndex == -1) {audioTrackIndex = i;hasAudio = true;// 获取音频格式信息AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_SAMPLE_RATE, &audioSampleRate);AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_CHANNEL_COUNT, &audioChannelCount);LOGI("Audio track: sampleRate=%d, channels=%d",audioSampleRate, audioChannelCount);LOGI("Selected audio track: %d", audioTrackIndex);callbackInfo ="Selected audio track:" + to_string(audioTrackIndex) + "\n";callbackInfo = callbackInfo + ",audioSampleRate:" + to_string(audioSampleRate)+ ",audioChannelCount:" + to_string(audioChannelCount) + "\n";PostStatusMessage(callbackInfo.c_str());}}AMediaFormat_delete(format);}return hasVideo || hasAudio;
}// 初始化复用器
bool MediaExtratorDecodec::initDecodec(bool asyncMode) {// 添加视频轨道if (hasVideo) {AMediaExtractor_selectTrack(extractor, videoTrackIndex);mVideoFormat = AMediaExtractor_getTrackFormat(extractor, videoTrackIndex);AMediaFormat_getString(mVideoFormat, AMEDIAFORMAT_KEY_MIME, &video_mime);LOGI("video_mime: %s", video_mime);callbackInfo ="video_mime:" + string(video_mime) + "\n";PostStatusMessage(callbackInfo.c_str());mVideoCodec = createMediaCodec(mVideoFormat, video_mime, "", false /*isEncoder*/);if (!mVideoCodec) {LOGE("Failed to create video codec");callbackInfo ="Failed to create video codec \n";PostStatusMessage(callbackInfo.c_str());return false;}if (asyncMode) {AMediaCodecOnAsyncNotifyCallback aCB = {OnInputAvailableCB, OnOutputAvailableCB,OnFormatChangedCB, OnErrorCB};AMediaCodec_setAsyncNotifyCallback(mVideoCodec, aCB, this);ThreadTask task = []() {CallBackHandle();};g_threadManager->submitTask("video-decode-Thread", task, PRIORITY_NORMAL);}LOGI("create video codec success");callbackInfo ="create video codec success:  \n";PostStatusMessage(callbackInfo.c_str());AMediaCodec_start(mVideoCodec);}// 添加音频轨道if (hasAudio) {AMediaExtractor_selectTrack(extractor, audioTrackIndex);mAudioFormat = AMediaExtractor_getTrackFormat(extractor, audioTrackIndex);AMediaFormat_getString(mAudioFormat, AMEDIAFORMAT_KEY_MIME, &audio_mime);LOGI("audio_mime: %s", audio_mime);callbackInfo ="audio_mime:" + string(audio_mime) + "\n";PostStatusMessage(callbackInfo.c_str());mAudioCodec = createMediaCodec(mAudioFormat, audio_mime, "", false /*isEncoder*/);if (!mAudioCodec) {LOGE("Failed to create audio codec");callbackInfo ="Failed to create audio codec \n";PostStatusMessage(callbackInfo.c_str());return false;}if (asyncMode) {AMediaCodecOnAsyncNotifyCallback aCB = {OnInputAvailableCB, OnOutputAvailableCB,OnFormatChangedCB, OnErrorCB};AMediaCodec_setAsyncNotifyCallback(mAudioCodec, aCB, this);ThreadTask task = []() {CallBackHandle();};g_threadManager->submitTask("audio-decode-Thread", task, PRIORITY_NORMAL);}LOGI("create audio codec success");callbackInfo ="create audio codec success:  \n";PostStatusMessage(callbackInfo.c_str());AMediaCodec_start(mAudioCodec);}LOGI("initDecodec initialized successfully");callbackInfo ="initDecodec initialized successfully \n";PostStatusMessage(callbackInfo.c_str());return true;
}// 执行解码
bool MediaExtratorDecodec::decodec() {LOGI("decodec===========");bool asyncMode = false;AMediaCodecBufferInfo info;bool sawEOS = false;int64_t lastVideoPts = -1;int64_t lastAudioPts = -1;// 重新选择所有轨道以重置读取位置if (hasVideo) AMediaExtractor_selectTrack(extractor, videoTrackIndex);if (hasAudio) AMediaExtractor_selectTrack(extractor, audioTrackIndex);// 设置读取位置到开始AMediaExtractor_seekTo(extractor, 0, AMEDIAEXTRACTOR_SEEK_CLOSEST_SYNC);while (!sawEOS) {ssize_t trackIndex = AMediaExtractor_getSampleTrackIndex(extractor);if (trackIndex < 0) {sawEOS = true;break;}if (trackIndex == videoTrackIndex && hasVideo) {// 检查时间戳是否有效(避免重复或倒退的时间戳)if (AMediaExtractor_getSampleTime(extractor) > lastVideoPts) {if (!asyncMode) {while (!mSawOutputEOS && !mSignalledError) {/* Queue input data */if (!mSawInputEOS) {ssize_t inIdx = AMediaCodec_dequeueInputBuffer(mVideoCodec,kQueueDequeueTimeoutUs);if (inIdx < 0 && inIdx != AMEDIACODEC_INFO_TRY_AGAIN_LATER) {LOGE("AMediaCodec_dequeueInputBuffer returned invalid index %zd\n",inIdx);mErrorCode = (media_status_t) inIdx;return mErrorCode;} else if (inIdx >= 0) {onInputAvailable(mVideoCodec, inIdx);}}/* Dequeue output data */AMediaCodecBufferInfo info;ssize_t outIdx = AMediaCodec_dequeueOutputBuffer(mVideoCodec, &info,kQueueDequeueTimeoutUs);if (outIdx == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {mVideoFormat = AMediaCodec_getOutputFormat(mVideoCodec);const char *s = AMediaFormat_toString(mVideoFormat);LOGI("Output format: %s\n", s);} else if (outIdx >= 0) {onOutputAvailable(mVideoCodec, outIdx, &info);} else if (!(outIdx == AMEDIACODEC_INFO_TRY_AGAIN_LATER ||outIdx == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED)) {LOGE("AMediaCodec_dequeueOutputBuffer returned invalid index %zd\n",outIdx);mErrorCode = (media_status_t) outIdx;return mErrorCode;}}} else {unique_lock<mutex> lock(mMutex);mDecoderDoneCondition.wait(lock, [this]() {return (mSawOutputEOS || mSignalledError);});}if (mSignalledError) {LOGE("Received Error while Decoding");return mErrorCode;}lastVideoPts = info.presentationTimeUs;}} else if (trackIndex == audioTrackIndex && hasAudio) {     //音频轨道的解码// 检查时间戳是否有效if (info.presentationTimeUs > lastAudioPts) {// 检查时间戳是否有效(避免重复或倒退的时间戳)if (!asyncMode) {while (!mSawOutputEOS && !mSignalledError) {/* Queue input data */if (!mSawInputEOS) {ssize_t inIdx = AMediaCodec_dequeueInputBuffer(mAudioCodec,kQueueDequeueTimeoutUs);if (inIdx < 0 && inIdx != AMEDIACODEC_INFO_TRY_AGAIN_LATER) {LOGE("AMediaCodec_dequeueInputBuffer returned invalid index %zd\n",inIdx);mErrorCode = (media_status_t) inIdx;return mErrorCode;} else if (inIdx >= 0) {onInputAvailable(mAudioCodec, inIdx);}}/* Dequeue output data */AMediaCodecBufferInfo info;ssize_t outIdx = AMediaCodec_dequeueOutputBuffer(mAudioCodec, &info,kQueueDequeueTimeoutUs);if (outIdx == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {mAudioFormat = AMediaCodec_getOutputFormat(mAudioCodec);const char *s = AMediaFormat_toString(mAudioFormat);LOGI("Output format: %s\n", s);} else if (outIdx >= 0) {onOutputAvailable(mVideoCodec, outIdx, &info);} else if (!(outIdx == AMEDIACODEC_INFO_TRY_AGAIN_LATER ||outIdx == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED)) {LOGE("AMediaCodec_dequeueOutputBuffer returned invalid index %zd\n",outIdx);mErrorCode = (media_status_t) outIdx;return mErrorCode;}}} else {unique_lock<mutex> lock(mMutex);mDecoderDoneCondition.wait(lock, [this]() {return (mSawOutputEOS || mSignalledError);});}if (mSignalledError) {ALOGE("Received Error while Decoding");return mErrorCode;}lastAudioPts = info.presentationTimeUs;}}// 短暂休眠以避免过度占用CPUstd::this_thread::sleep_for(std::chrono::milliseconds(1));}LOGI("media decodec completed");callbackInfo ="media decodec completed \n";PostStatusMessage(callbackInfo.c_str());return true;
}// 释放资源
void MediaExtratorDecodec::release() {if (extractor) {AMediaExtractor_delete(extractor);extractor = nullptr;}if (mVideoFormat) {AMediaFormat_delete(mVideoFormat);mVideoFormat = nullptr;}if (mVideoCodec) {AMediaCodec_stop(mVideoCodec);AMediaCodec_delete(mVideoCodec);}if (mAudioCodec) {AMediaCodec_stop(mAudioCodec);AMediaCodec_delete(mAudioCodec);}if (mAudioFormat) {AMediaFormat_delete(mAudioFormat);mAudioFormat = nullptr;}LOGI("Resources released");
}void MediaExtratorDecodec::onInputAvailable(AMediaCodec *mediaCodec, int32_t bufIdx) {LOGD("onInputAvailable %s", __func__);if (mediaCodec == mVideoCodec && mediaCodec) {if (mSawInputEOS || bufIdx < 0) return;if (mSignalledError) {CallBackHandle::mSawError = true;mDecoderDoneCondition.notify_one();return;}size_t bufSize = AMediaExtractor_getSampleSize(extractor);if (bufSize <= 0) {LOGE("AMediaExtractor_getSampleSize====");return;}// 获取输入缓冲区uint8_t *buf = AMediaCodec_getInputBuffer(mVideoCodec, bufIdx, &bufSize);if (!buf) {mErrorCode = AMEDIA_ERROR_IO;mSignalledError = true;mDecoderDoneCondition.notify_one();return;}// 从提取器读取数据ssize_t bytesRead = AMediaExtractor_readSampleData(extractor, buf, bufSize);if (bytesRead < 0) {LOGI("reading video sample data: %zd", bytesRead);// 输入结束AMediaCodec_queueInputBuffer(mVideoCodec, bufIdx, 0, 0, 0,AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM);LOGI("从提取器读取数据到结束尾");callbackInfo ="视频轨道从提取器读取数据到结束尾 reading sample data:" + to_string(bytesRead) +"\n";PostStatusMessage(callbackInfo.c_str());return;}uint32_t flag = AMediaExtractor_getSampleFlags(extractor);int64_t presentationTimeUs = AMediaExtractor_getSampleTime(extractor);if (flag == AMEDIA_ERROR_MALFORMED) {mErrorCode = (media_status_t) flag;mSignalledError = true;mDecoderDoneCondition.notify_one();return;}if (flag == AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) mSawInputEOS = true;LOGD("video - %s bytesRead : %zd presentationTimeUs : %" PRId64 " mSawInputEOS : %s",__FUNCTION__,bytesRead, presentationTimeUs, mSawInputEOS ? "TRUE" : "FALSE");// 将数据送入解码器media_status_t status = AMediaCodec_queueInputBuffer(mVideoCodec, bufIdx, 0 /* offset */,bytesRead, presentationTimeUs, flag);if (AMEDIA_OK != status) {mErrorCode = status;mSignalledError = true;mDecoderDoneCondition.notify_one();return;}if (!AMediaExtractor_advance(extractor)) {return;}} else if (mediaCodec == mAudioCodec && mediaCodec) {if (mSawInputEOS || bufIdx < 0) return;if (mSignalledError) {CallBackHandle::mSawError = true;mDecoderDoneCondition.notify_one();return;}size_t bufSize = AMediaExtractor_getSampleSize(extractor);if (bufSize <= 0) {LOGE("AMediaExtractor_getSampleSize====");return;}// 获取输入缓冲区uint8_t *buf = AMediaCodec_getInputBuffer(mAudioCodec, bufIdx, &bufSize);if (!buf) {mErrorCode = AMEDIA_ERROR_IO;mSignalledError = true;mDecoderDoneCondition.notify_one();return;}// 从提取器读取数据ssize_t bytesRead = AMediaExtractor_readSampleData(extractor, buf, bufSize);if (bytesRead < 0) {LOGI("reading audio sample data: %zd", bytesRead);// 输入结束AMediaCodec_queueInputBuffer(mAudioCodec, bufIdx, 0, 0, 0,AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM);LOGI("从提取器读取音频轨道数据到结束尾");callbackInfo ="音频轨道从提取器读取数据到结束尾 reading sample data:" + to_string(bytesRead) +"\n";PostStatusMessage(callbackInfo.c_str());return;}uint32_t flag = AMediaExtractor_getSampleFlags(extractor);int64_t presentationTimeUs = AMediaExtractor_getSampleTime(extractor);if (flag == AMEDIA_ERROR_MALFORMED) {mErrorCode = (media_status_t) flag;mSignalledError = true;mDecoderDoneCondition.notify_one();return;}if (flag == AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) mSawInputEOS = true;LOGD("audio - %s bytesRead : %zd presentationTimeUs : %" PRId64 " mSawInputEOS : %s",__FUNCTION__,bytesRead, presentationTimeUs, mSawInputEOS ? "TRUE" : "FALSE");// 将数据送入解码器media_status_t status = AMediaCodec_queueInputBuffer(mAudioCodec, bufIdx, 0 /* offset */,bytesRead, presentationTimeUs, flag);if (AMEDIA_OK != status) {mErrorCode = status;mSignalledError = true;mDecoderDoneCondition.notify_one();return;}if (!AMediaExtractor_advance(extractor)) {return;}}}void MediaExtratorDecodec::onOutputAvailable(AMediaCodec *mediaCodec, int32_t bufIdx,AMediaCodecBufferInfo *bufferInfo) {LOGD("In %s", __func__);if (mediaCodec == mVideoCodec && mediaCodec) {if (mSawOutputEOS || bufIdx < 0) return;if (mSignalledError) {CallBackHandle::mSawError = true;mDecoderDoneCondition.notify_one();return;}if (mOutFp != nullptr) {size_t bufSize;uint8_t *buf = AMediaCodec_getOutputBuffer(mVideoCodec, bufIdx, &bufSize);if (buf) {fwrite(buf, sizeof(char), bufferInfo->size, mOutFp);LOGD("bytes written into file  %d\n", bufferInfo->size);}}AMediaCodec_releaseOutputBuffer(mVideoCodec, bufIdx, false);mSawOutputEOS = (0 != (bufferInfo->flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM));mNumOutputVideoFrame++;LOGD("video - %s index : %d  mSawOutputEOS : %s count : %u", __FUNCTION__, bufIdx,mSawOutputEOS ? "TRUE" : "FALSE", mNumOutputVideoFrame);if (mSawOutputEOS) {CallBackHandle::mIsDone = true;mDecoderDoneCondition.notify_one();}} else if (mediaCodec == mAudioCodec && mediaCodec) {if (mSawOutputEOS || bufIdx < 0) return;if (mSignalledError) {CallBackHandle::mSawError = true;mDecoderDoneCondition.notify_one();return;}if (mOutFp != nullptr) {size_t bufSize;uint8_t *buf = AMediaCodec_getOutputBuffer(mAudioCodec, bufIdx, &bufSize);if (buf) {fwrite(buf, sizeof(char), bufferInfo->size, mOutFp);LOGD("bytes written into file  %d\n", bufferInfo->size);}}AMediaCodec_releaseOutputBuffer(mAudioCodec, bufIdx, false);mSawOutputEOS = (0 != (bufferInfo->flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM));mNumOutputAudioFrame++;LOGD("video - %s index : %d  mSawOutputEOS : %s count : %u", __FUNCTION__, bufIdx,mSawOutputEOS ? "TRUE" : "FALSE", mNumOutputAudioFrame);if (mSawOutputEOS) {CallBackHandle::mIsDone = true;mDecoderDoneCondition.notify_one();}}
}void MediaExtratorDecodec::onFormatChanged(AMediaCodec *mediaCodec, AMediaFormat *format) {LOGD("In %s", __func__);if (mediaCodec == mVideoCodec && mediaCodec) {LOGD("%s { %s }", __FUNCTION__, AMediaFormat_toString(format));mVideoFormat = format;}if (mediaCodec == mAudioCodec && mediaCodec) {LOGD("%s { %s }", __FUNCTION__, AMediaFormat_toString(format));mAudioFormat = format;}
}void MediaExtratorDecodec::onError(AMediaCodec *mediaCodec, media_status_t err) {LOGD("In %s", __func__);if (mediaCodec == mVideoCodec && mediaCodec) {ALOGE("Received Error %d", err);mErrorCode = err;mSignalledError = true;mDecoderDoneCondition.notify_one();}
}JNIEnv *MediaExtratorDecodec::GetJNIEnv(bool *isAttach) {JNIEnv *env;int status;if (nullptr == mJavaVm) {LOGD("GetJNIEnv mJavaVm == nullptr");return nullptr;}*isAttach = false;status = mJavaVm->GetEnv((void **) &env, JNI_VERSION_1_6);if (status != JNI_OK) {status = mJavaVm->AttachCurrentThread(&env, nullptr);if (status != JNI_OK) {LOGD("GetJNIEnv failed to attach current thread");return nullptr;}*isAttach = true;}return env;
}void MediaExtratorDecodec::PostStatusMessage(const char *msg) {bool isAttach = false;JNIEnv *pEnv = GetJNIEnv(&isAttach);if (pEnv == nullptr) {return;}jobject javaObj = mJavaObj;jmethodID mid = pEnv->GetMethodID(pEnv->GetObjectClass(javaObj), "CppStatusCallback","(Ljava/lang/String;)V");jstring pJstring = pEnv->NewStringUTF(msg);pEnv->CallVoidMethod(javaObj, mid, pJstring);if (isAttach) {JavaVM *pJavaVm = mJavaVm;pJavaVm->DetachCurrentThread();}
}

三.FFmpeg/MediaCodec解码过程对比:

         FFmpeg:

——> 打开输入文件获取输入文件的封装实例AVFormatContext :avformat_open_input(&in_fmt_ctx, srcPath, nullptr, nullptr);

        ——> 查找音视频文件中的流信息:avformat_find_stream_info(in_fmt_ctx, nullptr);

        ——> 分别查找音频和视频的索引:av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0) / av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0) ;

        ——> 分别查找流封装的实例AVStream:src_video = in_fmt_ctx->streams[video_index]

 / src_audio = in_fmt_ctx->streams[audio_index] ;

      ——> 这里是从媒体文件中封装实例AVFormatContext,查找音视频文件中的流信息 avformat_find_stream_info(in_fmt_ctx, nullptr)

                ——> 从封装实例中查找视频流的索引video_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);

                ——> 查找视频流AVStream,src_video = in_fmt_ctx->streams[video_index]。

                ——> 根据编解码器ID,查找解码器AVCodec *video_codec = (AVCodec *) avcodec_find_decoder(video_codec_id);

        ——> 把未解压的数据包发给解码器实例avcodec_send_packet(video_decode_ctx, packet);

        ——> 从解码器实例获取还原后的数据帧avcodec_receive_frame(video_decode_ctx, frame);

        MediaCodec:

          ——> 创建 AMediaExtractor_new()

        ——> 设置Extractor的fd AMediaExtractor_setDataSourceFd(extractor, input_fd, 0, fileSize)

        ——>  AMediaExtractor_getTrackCount()获取取器Extractor中的轨道数

        ——> AMediaExtractor_getTrackFormat(extractor, i) 遍历轨道从中获取每个轨道中的AMediaFormat

        ——> 筛选出音频轨道和视频轨道,分别获取音频轨道的audioTrackIndex值和视频轨道videoTrackIndex值。

        ——> 切换Extractor音视频的轨道,AMediaExtractor_getTrackFormat(extractor, videoTrackIndex) 方法中获取AMediaFormat

        ——> 通过AMediaCodec_createDecoderByType创建AMediaCodec

,并设置编解码AMediaCodec_configure(codec, format, nullptr, nullptr, isEncoder)。

       ——> 重新选择所有轨道以重置读取位置 AMediaExtractor_selectTrack(extractor, videoTrackIndex) 和 AMediaExtractor_selectTrack(extractor, audioTrackIndex);

        ——> 设置读取位置到开始 AMediaExtractor_seekTo(extractor, 0,         AMEDIAEXTRACTOR_SEEK_CLOSEST_SYNC);

        ——>遍历可用的输入和可用的输出

四.效果展示:

        

以上的代码放在本人的GitHub项目:https://github.com/wangyongyao1989/FFmpegPractices
http://www.dtcms.com/a/432238.html

相关文章:

  • 03.动画眼睛跟随鼠标光标 | JavaScript 鼠标移动事件
  • 瑞安学校网站建设哈尔滨网站优化技术
  • 南和邢台网站制作色彩设计网站
  • tauri2使用fs的watch报错fs:watch “Command watch not found“
  • 国外优秀vi设计网站eclipse网站建设
  • 扬中网站优化哪家好服务器2003怎么做网站
  • 深圳建站公司服务宁乡网页设计
  • 营销型网站模板广告代理商是什么意思
  • 网站建设需要几步让网站降权
  • 如何优化企业网站游戏网站创建
  • 单页网站seo怎么做想学设计没有基础怎么办
  • 泛解析对网站的影响网站建设的培训心得
  • 做企业网站的缺点英文外贸网站制作
  • 付网站建设服务费的会计分录深圳做推广哪家比较好
  • 莆系医疗网站建设做网站小代码大全
  • 23.CSS 图片悬停效果
  • 潍坊有哪些网站知名网站建设托管
  • 普通企业网站营销内链好的网站
  • 网站建设最新外文翻译网页传奇哪个最火
  • 外贸型网站建设方法企业信用报告如何获取
  • 网站建设小结wordpress 多地址插件
  • 报名网站建设费用报价做网站和做app哪个简单
  • 做seo的网站有那些广州 建设 招聘信息网站
  • Oralce取消账户锁定
  • 医院网站建设的特点招聘网站建设费用多少钱
  • 神农架网站建设wordpress博客头图怎么改
  • 佛山做外贸网站的公司吗什么企业做网站比较方便呢
  • 长沙公司网站的建设网站建设网站多少钱
  • 网上有哪些接单做效果图的网站如何做翻唱网站
  • 深圳手机网站建设价格低做网站比较好的数字