热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

视频提取图片/图片合成视频ffmpeg(二十)

前言需求场景1(视频中提取照片):各大网站在线播放视频时,鼠标滑到某一时刻能够提前显示那一时刻的画面。短的视频编辑APP中,为了更好的对

前言


  • 需求场景1(视频中提取照片):
    各大网站在线播放视频时,鼠标滑到某一时刻能够提前显示那一时刻的画面。短的视频编辑APP中,为了更好的对视频进行编辑,会提取出视频各个时刻的画面进行预览,那么这些是如何实现的呢?本文将给出基于ffmpeg的实现代码以及实现思路。
  • 需求场景2(照片合成视频):
    摄影师经常不间断的拍摄一组连续的画面用于合成延时视频,剪印APP中也有时光相册这样通过照片生成视频的功能(不过剪印APP照片合成的视频采用了插值算法生成了额外的过度动画照片以及特效,功能更加复杂,但是不管怎样,最终还是会由照片合成视频)。本文基于ffmpeg实现简单的照片合成视频思路以及详细代码

实现思路分析

这里照片以JPG为例,视频以MP4为例,其它格式类似


  • 视频中提取照片:

1、fmpeg对将像素数据写入到JPG图片中也封装到了avformat_xxx系列接口中,它的使用流程和封装视频数据到mp4文件一模一样,只不过一个JPG文件中只包含了一帧视频数据而已;
2、ffmpeg对JPG文件的封装支持模式匹配,即如果想要将多张图片写入到多张jpg中只需要文件名包含百分号即可,例如 name%3d.jpg,那么在每一次调用av_write_frame()函数写入视频数据到jpg图片时都会生成一张jpg图片。这样做的好处是不需要每一张要写入的jpg文件都创建一个AVFormatContext与之对应。其它流程和写入一张jpg一样。


流程为:
1、先从MP4中提取指定时刻AVPacket解码成AVFrame
2、然后将步骤1得到的AVFrame进行从素格式YUV420P到JPG需要的YUVJ420P像素格式的转换
3、再重新编码,然后再封装到jpg中


  • 照片合成视频:
    因为JPG的编码方式为AV_CODEC_ID_MJPEG,MP4如果采用h264编码,那么两者的编码方式是不一致的,所以就需要先解码再编码,具体流程为:
    1、先将JPG解码成AVFrame
    2、将JPG解码后的源像素格式YUVJ420P转换成x264编码需要的YUV420P像素格式
    3、再重新编码,然后再封装到mp4中

流程图


  • 视频中提取照片流程图

     

    #include "cppcommon/CLog.h"
    #include
    #include
    #include
    #include
    #include
    #include
    }
    using namespace std;class VideoJPG
    {
    public:VideoJPG();~VideoJPG();/** 功能:实现提取任意时刻视频的某一帧并将其转化为JPG图片输出到当前目录*/void doJpgGet();/** 功能:将多张JPG照片合并成一段视频*/void doJpgToVideo();
    private:AVFrame *de_frame;AVFrame *en_frame;// 用于视频像素转换SwsContext *sws_ctx;// 用于读取视频AVFormatContext *in_fmt;// 用于解码AVCodecContext *de_ctx;// 用于编码AVCodecContext *en_ctx;// 用于封装jpgAVFormatContext *ou_fmt;int video_ou_index;void releaseSources();void doDecode(AVPacket *in_pkt);void doEncode(AVFrame *en_frame);};

    备注:
    视频提取图片的实现位于doJpgGet();函数,图片合成视频位于doJpgToVideo();函数

    实现文件

    #include "VideoJpg.hpp"VideoJPG::VideoJPG()
    {sws_ctx = NULL;de_frame = NULL;en_frame = NULL;in_fmt = NULL;ou_fmt = NULL;de_ctx = NULL;en_ctx = NULL;
    }VideoJPG::~VideoJPG()
    {}void VideoJPG::releaseSources()
    {if (in_fmt) {avformat_close_input(&in_fmt);in_fmt = NULL;}if (ou_fmt) {avformat_free_context(ou_fmt);ou_fmt = NULL;}if (en_frame) {av_frame_unref(en_frame);en_frame = NULL;}if (de_frame) {av_frame_unref(de_frame);de_frame = NULL;}if (en_ctx) {avcodec_free_context(&en_ctx);en_ctx = NULL;}if (de_ctx) {avcodec_free_context(&de_ctx);de_ctx = NULL;}
    }/** 写入jpg说明:* 1、ffmpeg对将像素数据写入到JPG图片中也封装到了avformat_xxx系列接口中,它的使用流程和封装视频数据到mp4文件一模一样* 只不过一个JPG文件中只包含了一帧视频数据而已;* 2、ffmpeg对JPG文件的封装支持模式匹配,即如果想要将多张图片写入到多张jpg中只需要文件名包含百分号即可,例如 name%3d.jpg,那么在每一次调用av_write_frame()* 函数写入视频数据到jpg图片时都会生成一张jpg图片。这样做的好处是不需要每一张要写入的jpg文件都创建一个AVFormatContext与之对应。其它流程和写入一张jpg一样,具体* 参考如下示例:* 3、jpg对应的封装器为ff_image2_muxer,对应的编码器为ff_mjpeg_encoder*/
    #define Get_More 1 // 1代表使用模式匹配,一次可以写入多张jpg图片。0代表一次写入1张图片
    void VideoJPG::doJpgGet()
    {string curFile(__FILE__);unsigned long pos = curFile.find("2-video_audio_advanced");if (pos == string::npos) {LOGD("not find file");return;}string srcDic = curFile.substr(0,pos) + "filesources/";string srcPath = srcDic + "test_1280x720_3.mp4";
    #if Get_Morestring dstPath = srcDic + "1-doJpg_get%3d.jpg";int num = 5;
    #elsestring dstPath = srcDic + "1-doJpg_get.jpg";
    #endifint video_index &#61; -1;// 要截取的时刻string start &#61; "00:00:05";int64_t start_pts &#61; stoi(start.substr(0,2));start_pts &#43;&#61; stoi(start.substr(3,2));start_pts &#43;&#61; stoi(start.substr(6,2));if (avformat_open_input(&in_fmt,srcPath.c_str(),NULL,NULL) <0) {LOGD("avformat_open_input fail");return;}if (avformat_find_stream_info(in_fmt, NULL) <0) {LOGD("avformat_find_stream_info fail");return;}// 遍历出视频索引for (int i &#61; 0; inb_streams; i&#43;&#43;) {AVStream *stream &#61; in_fmt->streams[i];if (stream->codecpar->codec_type &#61;&#61; AVMEDIA_TYPE_VIDEO) { // 说明是视频video_index &#61; i;// 初始化解码器用于解码AVCodec *codec &#61; avcodec_find_decoder(stream->codecpar->codec_id);de_ctx &#61; avcodec_alloc_context3(codec);if (!de_ctx) {LOGD("video avcodec_alloc_context3 fail");releaseSources();return;}// 设置解码参数&#xff0c;这里直接从源视频流中拷贝if (avcodec_parameters_to_context(de_ctx, stream->codecpar) <0) {LOGD("video avcodec_parameters_to_context");releaseSources();return;}// 初始化解码器上下文if (avcodec_open2(de_ctx, codec, NULL) <0) {LOGD("video avcodec_open2() fail");releaseSources();return;}break;}}// 初始化编码器;因为最终是要写入到JPEG&#xff0c;所以使用的编码器ID为AV_CODEC_ID_MJPEGAVCodec *codec &#61; avcodec_find_encoder(AV_CODEC_ID_MJPEG);en_ctx &#61; avcodec_alloc_context3(codec);if (!en_ctx) {LOGD("avcodec_alloc_context3 fail");releaseSources();return;}// 设置编码参数AVStream *in_stream &#61; in_fmt->streams[video_index];en_ctx->width &#61; in_stream->codecpar->width;en_ctx->height &#61; in_stream->codecpar->height;// 如果是编码后写入到图片中&#xff0c;那么比特率可以不用设置&#xff0c;不影响最终的结果(也不会影响图像清晰度)en_ctx->bit_rate &#61; in_stream->codecpar->bit_rate;// 如果是编码后写入到图片中&#xff0c;那么帧率可以不用设置&#xff0c;不影响最终的结果en_ctx->framerate &#61; in_stream->r_frame_rate;en_ctx->time_base &#61; in_stream->time_base;// 对于MJPEG编码器来说&#xff0c;它支持的是YUVJ420P/YUVJ422P/YUVJ444P格式的像素en_ctx->pix_fmt &#61; AV_PIX_FMT_YUVJ420P;// 初始化编码器上下文if (avcodec_open2(en_ctx, codec, NULL) <0) {LOGD("avcodec_open2() fail");releaseSources();return;}// 创建用于输出JPG的封装器if (avformat_alloc_output_context2(&ou_fmt, NULL, NULL, dstPath.c_str()) <0) {LOGD("avformat_alloc_output_context2");releaseSources();return;}/** 添加流* 对于图片封装器来说&#xff0c;也可以把它想象成只有一帧视频的视频封装器。所以它实际上也需要一路视频流&#xff0c;而事实上图片的流是视频流类型*/AVStream *stream &#61; avformat_new_stream(ou_fmt, NULL);// 设置流参数&#xff1b;直接从编码器拷贝参数即可if (avcodec_parameters_from_context(stream->codecpar, en_ctx) <0) {LOGD("avcodec_parameters_from_context");releaseSources();return;}/** 初始化上下文* 对于写入JPG来说&#xff0c;它是不需要建立输出上下文IO缓冲区的的&#xff0c;所以avio_open2()没有调用到&#xff0c;但是最终一样可以调用av_write_frame()写入数据*/if (!(ou_fmt->oformat->flags & AVFMT_NOFILE)) {if (avio_open2(&ou_fmt->pb, dstPath.c_str(), AVIO_FLAG_WRITE, NULL, NULL) <0) {LOGD("avio_open2 fail");releaseSources();return;}}/** 为输出文件写入头信息* 不管是封装音视频文件还是图片文件&#xff0c;都需要调用此方法进行相关的初始化&#xff0c;否则av_write_frame()函数会崩溃*/if (avformat_write_header(ou_fmt, NULL) <0) {LOGD("avformat_write_header() fail");releaseSources();return;}/** 创建视频像素转换上下文* 因为源视频的像素格式是yuv420p的&#xff0c;而jpg编码需要的像素格式是yuvj420p的&#xff0c;所以需要先进行像素格式转换*/sws_ctx &#61; sws_getContext(in_stream->codecpar->width, in_stream->codecpar->height, (enum AVPixelFormat)in_stream->codecpar->format,en_ctx->width, en_ctx->height, en_ctx->pix_fmt,0, NULL, NULL, NULL);if (!sws_ctx) {LOGD("sws_getContext fail");releaseSources();return;}// 创建编码解码用的AVFramede_frame &#61; av_frame_alloc();en_frame &#61; av_frame_alloc();en_frame->width &#61; en_ctx->width;en_frame->height &#61; en_ctx->height;en_frame->format &#61; en_ctx->pix_fmt;if (av_frame_get_buffer(en_frame, 0) <0) {LOGD("av_frame_get_buffer fail");releaseSources();return;}if (av_frame_make_writable(en_frame) <0) {LOGD("av_frame_make_writeable fail");releaseSources();return;}AVPacket *in_pkt &#61; av_packet_alloc();AVPacket *ou_pkt &#61; av_packet_alloc();AVRational time_base &#61; in_fmt->streams[video_index]->time_base;AVRational frame_rate &#61; in_fmt->streams[video_index]->r_frame_rate;// 一帧的时间戳int64_t delt &#61; time_base.den/frame_rate.num;start_pts *&#61; time_base.den;/** 因为想要截取的时间处的AVPacket并不一定是I帧&#xff0c;所以想要正确的解码&#xff0c;得先找到离想要截取的时间处往前的最近的I帧* 开始解码&#xff0c;直到拿到了想要获取的时间处的AVFrame* AVSEEK_FLAG_BACKWARD 代表如果start_pts指定的时间戳处的AVPacket非I帧&#xff0c;那么就往前移动指针&#xff0c;直到找到I帧&#xff0c;那么* 当首次调用av_frame_read()函数时返回的AVPacket将为此I帧的AVPacket*/if (av_seek_frame(in_fmt, video_index, start_pts, AVSEEK_FLAG_BACKWARD) <0) {LOGD("av_seek_frame fail");releaseSources();return;}bool found &#61; false;while (av_read_frame(in_fmt, in_pkt) &#61;&#61; 0) {if (in_pkt->stream_index !&#61; video_index) {continue;}// 先解码avcodec_send_packet(de_ctx, in_pkt);LOGD("video pts %d(%s)",in_pkt->pts,av_ts2timestr(in_pkt->pts,&in_stream->time_base));while (true) {int ret &#61; avcodec_receive_frame(de_ctx, de_frame);if (ret <0) {break;}/** 解码得到的AVFrame中的pts和解码前的AVPacket中的pts是一一对应的&#xff0c;所以可以利用AVFrame中的pts来判断此帧是否在想要截取的时间范围内*/LOGD("sucess pts %d",de_frame->pts);// 成功解码出来了
    #if Get_More// 取多帧视频并写入到文件static int i&#61;0;delt &#61; delt*num;if (abs(de_frame->pts - start_pts) #else// 去一帧帧视频并写入到文件if (abs(de_frame->pts - start_pts) #endifLOGD("找到了这一帧");// 因为源视频帧的格式和目标视频帧的格式可能不一致&#xff0c;所以这里需要转码ret &#61; sws_scale(sws_ctx, de_frame->data, de_frame->linesize, 0, de_frame->height, en_frame->data, en_frame->linesize);if (ret <0) {LOGD("sws_scale fail");releaseSources();return;}#if Get_More// 重新编码en_frame->pts &#61; i;avcodec_send_frame(en_ctx, en_frame);// 拿到指定数目的AVPacket后再清空缓冲区if (i > num) {avcodec_send_frame(en_ctx, NULL);}
    #else// 重新编码;因为只有一帧&#xff0c;所以这里直接写1 即可en_frame->pts &#61; 1;avcodec_send_frame(en_ctx, en_frame);// 因为只编码一帧&#xff0c;所以发送一帧视频后立马清空缓冲区avcodec_send_frame(en_ctx, NULL);
    #endifret &#61; avcodec_receive_packet(en_ctx, ou_pkt);if (ret <0) {LOGD("fail ");releaseSources();return;}// 写入文件if(av_write_frame(ou_fmt, ou_pkt) <0) {LOGD("av_write_frame fail");releaseSources();return;}
    #if Get_Moreif (i>num) {found &#61; true;}#elsefound &#61; true;
    #endifbreak;}}av_packet_unref(in_pkt);if (found) {LOGD("has get jpg");break;}}/** 写入文件尾* 对于写入视频文件来说&#xff0c;此函数必须调用&#xff0c;但是对于写入JPG文件来说&#xff0c;不调用此函数也没关系&#xff1b;*/
    // av_write_trailer(ou_fmt);// 释放资源releaseSources();
    }/** 多张图片合成为一段视频说明&#xff1a;* 1、ffmpeg对jpg的解封装和对视频的解封装一样&#xff0c;都封装到了avformat_xxxx系列接口里面。流程和解封装音视频的流程一模一样,对一张jpg图片的解封装可以理解为对只包含一帧* 视频的视频文件的解封装* 2、ffmpeg对jpeg的解封装支持模式匹配&#xff0c;例如对于name%3d.jpg进行解封装时(假如目录中包含的jpg列表为* name001.jpg,name002.jpg,name004.jpg,........)&#xff0c;每次调用av_frame_read()函数&#xff0c;它将按照name001.jpg,name002.jpg,name004.jpg,........的顺序依次进行读取* 3、jpg对应的解封装器为ff_image2_demuxer&#xff0c;对应的编码器为ff_mjpeg_decoder*//** 将前面视频生成的jpg合成mp4文件* 因为JPG的编码方式为AV_CODEC_ID_MJPEG&#xff0c;MP4如果采用h264编码&#xff0c;那么两者的编码方式是不一致的&#xff0c;所以就需要先解码再编码&#xff0c;具体流程为&#xff1a;* 1、先将JPG解码成AVFrame* 2、将JPG解码后的源像素格式YUVJ420P转换成x264编码需要的YUV420P像素格式* 3、再重新编码&#xff0c;然后再封装到mp4中*/
    void VideoJPG::doJpgToVideo()
    {string curFile(__FILE__);unsigned long pos &#61; curFile.find("2-video_audio_advanced");if (pos &#61;&#61; string::npos) {LOGD("not find file");return;}string srcDic &#61; curFile.substr(0,pos) &#43; "filesources/";string srcPath &#61; srcDic &#43; "1-doJpg_get%3d.jpg";string dstPath &#61; srcDic &#43; "1-dojpgToVideo.mp4";int video_index &#61; -1;// 创建jpg的解封装上下文if (avformat_open_input(&in_fmt, srcPath.c_str(), NULL, NULL) <0) {LOGD("avformat_open_input fail");return;}if (avformat_find_stream_info(in_fmt, NULL) <0) {LOGD("avformat_find_stream_info()");releaseSources();return;}// 创建解码器及初始化解码器上下文用于对jpg进行解码for (int i&#61;0; inb_streams; i&#43;&#43;) {AVStream *stream &#61; in_fmt->streams[i];/** 对于jpg图片来说&#xff0c;它里面就是一路视频流&#xff0c;所以媒体类型就是AVMEDIA_TYPE_VIDEO*/if (stream->codecpar->codec_type &#61;&#61; AVMEDIA_TYPE_VIDEO) {AVCodec *codec &#61; avcodec_find_decoder(stream->codecpar->codec_id);if (!codec) {LOGD("not find jpg codec");releaseSources();return;}de_ctx &#61; avcodec_alloc_context3(codec);if (!de_ctx) {LOGD("jpg codec_ctx fail");releaseSources();return;}// 设置解码参数;文件解封装的AVStream中就包括了解码参数&#xff0c;这里直接流中拷贝即可if (avcodec_parameters_to_context(de_ctx, stream->codecpar) <0) {LOGD("set jpg de_ctx parameters fail");releaseSources();return;}// 初始化解码器及解码器上下文if (avcodec_open2(de_ctx, codec, NULL) <0) {LOGD("avcodec_open2() fail");releaseSources();return;}video_index &#61; i;break;}}// 创建mp4文件封装器if (avformat_alloc_output_context2(&ou_fmt,NULL,NULL,dstPath.c_str()) <0) {LOGD("MP2 muxer fail");releaseSources();return;}// 添加视频流AVStream *stream &#61; avformat_new_stream(ou_fmt, NULL);video_ou_index &#61; stream->index;// 创建h264的编码器及编码器上下文AVCodec *en_codec &#61; avcodec_find_encoder(AV_CODEC_ID_H264);if (!en_codec) {LOGD("encodec fail");releaseSources();return;}en_ctx &#61; avcodec_alloc_context3(en_codec);if (!en_ctx) {LOGD("en_codec ctx fail");releaseSources();return;}// 设置编码参数AVStream *in_stream &#61; in_fmt->streams[video_index];en_ctx->width &#61; in_stream->codecpar->width;en_ctx->height &#61; in_stream->codecpar->height;en_ctx->pix_fmt &#61; (enum AVPixelFormat)in_stream->codecpar->format;en_ctx->bit_rate &#61; 0.96*1000000;en_ctx->framerate &#61; (AVRational){5,1};en_ctx->time_base &#61; (AVRational){1,5};// 某些封装格式必须要设置&#xff0c;否则会造成封装后文件中信息的缺失if (ou_fmt->oformat->flags & AVFMT_GLOBALHEADER) {en_ctx->flags |&#61; AV_CODEC_FLAG_GLOBAL_HEADER;}// x264编码特有if (en_codec->id &#61;&#61; AV_CODEC_ID_H264) {// 代表了编码的速度级别av_opt_set(en_ctx->priv_data,"preset","slow",0);en_ctx->flags2 &#61; AV_CODEC_FLAG2_LOCAL_HEADER;}// 初始化编码器及编码器上下文if (avcodec_open2(en_ctx,en_codec,NULL) <0) {LOGD("encodec ctx fail");releaseSources();return;}// 设置视频流参数;对于封装来说&#xff0c;直接从编码器上下文拷贝即可if (avcodec_parameters_from_context(stream->codecpar, en_ctx) <0) {LOGD("copy en_code parameters fail");releaseSources();return;}// 初始化封装器输出缓冲区if (!(ou_fmt->oformat->flags & AVFMT_NOFILE)) {if (avio_open2(&ou_fmt->pb, dstPath.c_str(), AVIO_FLAG_WRITE, NULL, NULL) <0) {LOGD("avio_open2 fail");releaseSources();return;}}// 创建像素格式转换器sws_ctx &#61; sws_getContext(de_ctx->width, de_ctx->height, de_ctx->pix_fmt,en_ctx->width, en_ctx->height, en_ctx->pix_fmt,0, NULL, NULL, NULL);if (!sws_ctx) {LOGD("sws_getContext fail");releaseSources();return;}// 写入封装器头文件信息&#xff1b;此函数内部会对封装器参数做进一步初始化if (avformat_write_header(ou_fmt, NULL) <0) {LOGD("avformat_write_header fail");releaseSources();return;}// 创建编解码用的AVFramede_frame &#61; av_frame_alloc();en_frame &#61; av_frame_alloc();en_frame->width &#61; en_ctx->width;en_frame->height &#61; en_ctx->height;en_frame->format &#61; en_ctx->pix_fmt;av_frame_get_buffer(en_frame, 0);av_frame_make_writable(en_frame);AVPacket *in_pkt &#61; av_packet_alloc();while (av_read_frame(in_fmt, in_pkt) &#61;&#61; 0) {if (in_pkt->stream_index !&#61; video_index) {continue;}// 先解码doDecode(in_pkt);av_packet_unref(in_pkt);}// 刷新解码缓冲区doDecode(NULL);av_write_trailer(ou_fmt);LOGD("结束。。。");// 释放资源releaseSources();
    }void VideoJPG::doDecode(AVPacket *in_pkt)
    {static int num_pts &#61; 0;// 先解码avcodec_send_packet(de_ctx, in_pkt);while (true) {int ret &#61; avcodec_receive_frame(de_ctx, de_frame);if (ret &#61;&#61; AVERROR_EOF) {doEncode(NULL);break;} else if(ret <0) {break;}// 成功解码了&#xff1b;先进行格式转换然后再编码if(sws_scale(sws_ctx, de_frame->data, de_frame->linesize, 0, de_frame->height, en_frame->data, en_frame->linesize) <0) {LOGD("sws_scale fail");releaseSources();return;}// 编码前要设置好pts的值&#xff0c;如果en_ctx->time_base为{1,fps}&#xff0c;那么这里pts的值即为帧的个数值en_frame->pts &#61; num_pts&#43;&#43;;doEncode(en_frame);}}void VideoJPG::doEncode(AVFrame *en_frame1)
    {avcodec_send_frame(en_ctx, en_frame1);while (true) {AVPacket *ou_pkt &#61; av_packet_alloc();if (avcodec_receive_packet(en_ctx, ou_pkt) <0) {av_packet_unref(ou_pkt);break;}// 成功编码了;写入之前要进行时间基的转换AVStream *stream &#61; ou_fmt->streams[video_ou_index];av_packet_rescale_ts(ou_pkt, en_ctx->time_base, stream->time_base);LOGD("video pts %d(%s)",ou_pkt->pts,av_ts2timestr(ou_pkt->pts, &stream->time_base));av_write_frame(ou_fmt, ou_pkt);}
    }

     


    项目地址

    https://github.com/nldzsz/ffmpeg-demo

    位于cppsrc目录下
    VideoJpg.hpp/VideoJpg.cpp文件

    项目下示例可运行于iOS/android/mac平台&#xff0c;工程分别位于demo-ios/demo-android/demo-mac三个目录下&#xff0c;可根据需要选择不同平台


推荐阅读
  • 在《Cocos2d-x学习笔记:基础概念解析与内存管理机制深入探讨》中,详细介绍了Cocos2d-x的基础概念,并深入分析了其内存管理机制。特别是针对Boost库引入的智能指针管理方法进行了详细的讲解,例如在处理鱼的运动过程中,可以通过编写自定义函数来动态计算角度变化,利用CallFunc回调机制实现高效的游戏逻辑控制。此外,文章还探讨了如何通过智能指针优化资源管理和避免内存泄漏,为开发者提供了实用的编程技巧和最佳实践。 ... [详细]
  • Python 序列图分割与可视化编程入门教程
    本文介绍了如何使用 Python 进行序列图的快速分割与可视化。通过一个实际案例,详细展示了从需求分析到代码实现的全过程。具体包括如何读取序列图数据、应用分割算法以及利用可视化库生成直观的图表,帮助非编程背景的用户也能轻松上手。 ... [详细]
  • 在Android平台中,播放音频的采样率通常固定为44.1kHz,而录音的采样率则固定为8kHz。为了确保音频设备的正常工作,底层驱动必须预先设定这些固定的采样率。当上层应用提供的采样率与这些预设值不匹配时,需要通过重采样(resample)技术来调整采样率,以保证音频数据的正确处理和传输。本文将详细探讨FFMpeg在音频处理中的基础理论及重采样技术的应用。 ... [详细]
  • 在本文中,我们将为 HelloWorld 项目添加视图组件,以确保控制器返回的视图路径能够正确映射到指定页面。这一步骤将为后续的测试和开发奠定基础。首先,我们将介绍如何配置视图解析器,以便 SpringMVC 能够识别并渲染相应的视图文件。 ... [详细]
  • 利用PHP SDK高效接入新浪微博热搜榜单功能 ... [详细]
  • 基于OpenCV的图像拼接技术实践与示例代码解析
    图像拼接技术在全景摄影中具有广泛应用,如手机全景拍摄功能,通过将多张照片根据其关联信息合成为一张完整图像。本文详细探讨了使用Python和OpenCV库实现图像拼接的具体方法,并提供了示例代码解析,帮助读者深入理解该技术的实现过程。 ... [详细]
  • 本文介绍如何在 Android 中自定义加载对话框 CustomProgressDialog,包括自定义 View 类和 XML 布局文件的详细步骤。 ... [详细]
  • 网站访问全流程解析
    本文详细介绍了从用户在浏览器中输入一个域名(如www.yy.com)到页面完全展示的整个过程,包括DNS解析、TCP连接、请求响应等多个步骤。 ... [详细]
  • 基于Linux开源VOIP系统LinPhone[四]
    ****************************************************************************************** ... [详细]
  • 基于Net Core 3.0与Web API的前后端分离开发:Vue.js在前端的应用
    本文介绍了如何使用Net Core 3.0和Web API进行前后端分离开发,并重点探讨了Vue.js在前端的应用。后端采用MySQL数据库和EF Core框架进行数据操作,开发环境为Windows 10和Visual Studio 2019,MySQL服务器版本为8.0.16。文章详细描述了API项目的创建过程、启动步骤以及必要的插件安装,为开发者提供了一套完整的开发指南。 ... [详细]
  • Android 构建基础流程详解
    Android 构建基础流程详解 ... [详细]
  • 本文详细解析了使用C++实现的键盘输入记录程序的源代码,该程序在Windows应用程序开发中具有很高的实用价值。键盘记录功能不仅在远程控制软件中广泛应用,还为开发者提供了强大的调试和监控工具。通过具体实例,本文深入探讨了C++键盘记录程序的设计与实现,适合需要相关技术的开发者参考。 ... [详细]
  • 在《ChartData类详解》一文中,我们将深入探讨 MPAndroidChart 中的 ChartData 类。本文将详细介绍如何设置图表颜色(Setting Colors)以及如何格式化数据值(Formatting Data Values),通过 ValueFormatter 的使用来提升图表的可读性和美观度。此外,我们还将介绍一些高级配置选项,帮助开发者更好地定制和优化图表展示效果。 ... [详细]
  • Python 程序转换为 EXE 文件:详细解析 .py 脚本打包成独立可执行文件的方法与技巧
    在开发了几个简单的爬虫 Python 程序后,我决定将其封装成独立的可执行文件以便于分发和使用。为了实现这一目标,首先需要解决的是如何将 Python 脚本转换为 EXE 文件。在这个过程中,我选择了 Qt 作为 GUI 框架,因为之前对此并不熟悉,希望通过这个项目进一步学习和掌握 Qt 的基本用法。本文将详细介绍从 .py 脚本到 EXE 文件的整个过程,包括所需工具、具体步骤以及常见问题的解决方案。 ... [详细]
  • CSS3 @font-face 字体应用技术解析与实践
    在Web前端开发中,HTML教程和CSS3的结合使得网页设计更加多样化。长期以来,Web设计师受限于“web-safe”字体的选择。然而,CSS3中的`@font-face`规则允许从服务器端加载自定义字体,极大地丰富了网页的视觉效果。通过这一技术,设计师可以自由选择和使用各种字体,提升用户体验和页面美观度。本文将深入解析`@font-face`的实现原理,并提供实际应用案例,帮助开发者更好地掌握这一强大工具。 ... [详细]
author-avatar
elgin2010
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有