ffmpeg播放器開發 詳細記錄+程式碼實現3
請接上一個簡書內容觀看,會對上一章的程式碼進行改進和修改~~~
ffmpeg播放器3-音訊解碼與opensl es播放
1.ffmpeg 解碼音訊並播放的開發
實現 DNFFmpeg的 start()方法
//cmake中加入OpenSLES庫
target_link_libraries(XXX OpenSLES)
2.相關知識整理
簡書連結:
https://www.jianshu.com/p/e94652ee371c
Android 播放 PCM 有:1.Java SDK : AudioTrack (AudioRecorder錄音)
2.NDK : OPenSL ES (效率比較好)
RGB、YUV
:影象原始格式PCM
: 聲音的原始格式
取樣率
:1s 採集多少次聲音 採集頻率 HZ取樣位
:16bit 2位元組聲道數
:單聲道、雙聲道
大端
: 0xab 0xcd小端
: 0xcd 0xab
3.程式碼實現:
...之前的建立類:
建立 DNPlayer.java,用於呼叫so中的方法
建立 mylog.h, 用於定義一些巨集
建立 DNFFmpeg.h & .cpp ,用於提供方法給 native-lib.cpp 呼叫
建立 JavaCallHelper.h & .cpp ,用於so呼叫java的方法(即回撥)
建立 AudioChannel.h & .cpp ,用於音訊開發
建立 VideoChannel& .cpp ,用於視訊開發
... 新建立類:
safeQueue.h 使用者建立執行緒安全的佇列
BaseChannel.h 用於音訊和視訊開發類的父類,存放一些共用的方法和資訊
1. DNPlayer.java:
在surfaceChanged回撥中呼叫 native_setSurface(Surface surface)方法傳入 Surface
編寫start()方法 ->呼叫 native_start()方法
...
2. BaseChannel.h
#ifndef MYFFMPEGPLAYER_BASECHANNEL_H
#define MYFFMPEGPLAYER_BASECHANNEL_H
extern "C"{
#include <libavcodec/avcodec.h>
};
#include "safe_queue.h"
class BaseChannel {
public:
BaseChannel(int i,AVCodecContext *avCodecContext) : index(i),avCodecContext(avCodecContext) {
// 設定釋放佇列中的AVPacket 物件資料的方法回撥
packets.setReleaseCallBack(BaseChannel::releaseAVPacket);
// 設定釋放佇列中的 AVFrame 物件資料的方法回撥
frames.setReleaseCallBack(BaseChannel::releaseAVFrame);
};
virtual ~BaseChannel() {
packets.clear();
frames.clear();
};
// 釋放 AVPacket
static void releaseAVPacket(AVPacket*& packet){
if(packet){
av_packet_free(&packet);
packet = 0;
}
}
// 釋放 AVFrame
static void releaseAVFrame(AVFrame*& frame){
if(frame){
av_frame_free(&frame);
frame = 0;
}
}
// 抽象方法 解碼+播放
virtual void start() = 0;
int index;
// 執行緒安全的佇列 用於存放壓縮後的包
SafeQueue<AVPacket*> packets;
// 執行緒安全的佇列 用於存放解碼後的資料
SafeQueue<AVFrame*> frames;
// 解碼器上下文
AVCodecContext *avCodecContext = 0;
bool isPlaying;// 是否工作
};
#endif //MYFFMPEGPLAYER_BASECHANNEL_H
3.safe_queue.h
#ifndef DNRECORDER_SAFE_QUEUE_H
#define DNRECORDER_SAFE_QUEUE_H
#include <queue>
#include <pthread.h>
using namespace std;
//執行緒安全的佇列
template<typename T>
class SafeQueue {
typedef void (*ReleaseCallBack)(T &);
typedef void (*SyncHandle)(queue<T> &);
public:
SafeQueue() {
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
}
~SafeQueue() {
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
}
void push(const T new_value) {
pthread_mutex_lock(&mutex);
if (work) {
q.push(new_value);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
pthread_mutex_unlock(&mutex);
}
int pop(T &value) {
int ret = 0;
pthread_mutex_lock(&mutex);
//在多核處理器下 由於競爭可能虛假喚醒 包括jdk也說明了
while (work && q.empty()) {
pthread_cond_wait(&cond, &mutex);
}
if (!q.empty()) {
value = q.front();
q.pop();
ret = 1;
}
pthread_mutex_unlock(&mutex);
return ret;
}
void setWork(int work) {
pthread_mutex_lock(&mutex);
this->work = work;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
int empty() {
return q.empty();
}
int size() {
return q.size();
}
void clear() {
pthread_mutex_lock(&mutex);
int size = q.size();
for (int i = 0; i < size; ++i) {
T value = q.front();
releaseCallBack(value);
q.pop();
}
pthread_mutex_unlock(&mutex);
}
void sync() {
pthread_mutex_lock(&mutex);
// 同步程式碼塊 能線上程安全的背景下操作 queue :例如 主動丟包
syncHandle(q);
pthread_mutex_unlock(&mutex);
}
void setReleaseCallBack(ReleaseCallBack r) {
releaseCallBack = r;
}
void setSyncHandle(SyncHandle s) {
syncHandle = s;
}
private:
pthread_cond_t cond;
pthread_mutex_t mutex;
queue<T> q;
// 是否工作 1工作 0不工作
int work;
ReleaseCallBack releaseCallBack;
SyncHandle syncHandle;
};
#endif //DNRECORDER_SAFE_QUEUE_H
4.AudioChannel.h & AudioChannel.cpp
AudioChannel.h:
#ifndef MYFFMPEGPLAYER_AUDIOCHANNEL_H
#define MYFFMPEGPLAYER_AUDIOCHANNEL_H
#include "BaseChannel.h"
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
extern "C"{
#include <libswresample/swresample.h>
};
class AudioChannel : public BaseChannel{
public:
AudioChannel(int i, AVCodecContext *avCodecContext);
~AudioChannel();
// 解碼+播放
void start();
// 解碼
void decode();
// 播放
void play();
int getPcm();
uint8_t *data = 0;
int out_channels;
int out_samplesize;
int out_sample_rate;
private:
/**
* OpenSL ES
*/
// 引擎與引擎介面
SLObjectItf engineObject = 0;
SLEngineItf engineInterface = 0;
//混音器
SLObjectItf outputMixObject = 0;
//播放器
SLObjectItf bqPlayerObject = 0;
//播放器介面
SLPlayItf bqPlayerInterface = 0;
SLAndroidSimpleBufferQueueItf bqPlayerBufferQueueInterface =0;
//重取樣
SwrContext *swrContext = 0;
// 解碼執行緒
pthread_t pid_decode;
// 播放執行緒
pthread_t pid_play;
};
#endif //MYFFMPEGPLAYER_AUDIOCHANNEL_H
AudioChannel.cpp:
// 解碼執行緒
void *task_decodes(void *args) {
AudioChannel *audioChannel = static_cast<AudioChannel *>(args);
audioChannel->decode();
return 0;
}
// 播放執行緒
void *task_plays(void *args) {
AudioChannel *audioChannel = static_cast<AudioChannel *>(args);
audioChannel->play();
return 0;
}
AudioChannel::AudioChannel(int i, AVCodecContext *avCodecContext) : BaseChannel(i,
avCodecContext) {
out_channels = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);
out_samplesize = av_get_bytes_per_sample(AV_SAMPLE_FMT_S16);
out_sample_rate = 44100;
//44100個16位 44100 * 2
// 44100*(雙聲道)*(16位)
data = static_cast<uint8_t *>(malloc(out_sample_rate * out_channels * out_samplesize));
memset(data,0,out_sample_rate * out_channels * out_samplesize);
}
AudioChannel::~AudioChannel(){
if(data){
free(data);
data = 0;
}
}
//返回獲取的pcm資料大小
int AudioChannel::getPcm() {
int data_size = 0;
AVFrame *frame;
int ret = frames.pop(frame);
if (!isPlaying) {
if (ret) {
releaseAVFrame(frame);
}
return data_size;
}
//48000HZ 8位 =》 44100 16位
//重取樣
// 假設我們輸入了10個資料 ,swrContext轉碼器 這一次處理了8個資料
// 那麼如果不加delays(上次沒處理完的資料) , 積壓
int64_t delays = swr_get_delay(swrContext, frame->sample_rate);
// 將 nb_samples 個資料 由 sample_rate取樣率轉成 44100 後 返回多少個資料
// 10 個 48000 = nb 個 44100
// AV_ROUND_UP : 向上取整 1.1 = 2
int64_t max_samples = av_rescale_rnd(delays + frame->nb_samples, out_sample_rate,
frame->sample_rate, AV_ROUND_UP);
//上下文+輸出緩衝區+輸出緩衝區能接受的最大資料量+輸入資料+輸入資料個數
//返回 每一個聲道的輸出資料
int samples = swr_convert(swrContext, &data, max_samples, (const uint8_t **) frame->data,
frame->nb_samples);
//獲得 samples 個 * 2 聲道 * 2位元組(16位)
data_size = samples * out_samplesize * out_channels;
return data_size;
}
void AudioChannel::start() {
//0+輸出聲道+輸出取樣位+輸出取樣率+ 輸入的3個引數
swrContext = swr_alloc_set_opts(0, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, out_sample_rate,
avCodecContext->channel_layout, avCodecContext->sample_fmt,
avCodecContext->sample_rate, 0, 0);
//初始化
swr_init(swrContext);
isPlaying = 1;
packets.setWork(1);
frames.setWork(1);
//1.解碼
pthread_create(&pid_decode, NULL, task_decodes, this);
//2.播放
pthread_create(&pid_play, NULL, task_plays, this);
}
// 解碼
void AudioChannel::decode() {
AVPacket *avPacket = 0;
while (isPlaying) {
//取出一個資料包
int ret = packets.pop(avPacket);
if (!isPlaying) {
break;
}
if (!ret) {
continue;
}
// 把包丟給解碼器
ret = avcodec_send_packet(avCodecContext, avPacket);
releaseAVPacket(avPacket);
if (ret != 0) {
break;
}
AVFrame *avFrame = av_frame_alloc();
// 從解碼器中讀取解碼後的資料包
ret = avcodec_receive_frame(avCodecContext, avFrame);
if (ret == AVERROR(EAGAIN)) {
// 讀取失敗 需要重試
continue;
} else if (ret != 0) {
break;
}
frames.push(avFrame);
}
releaseAVPacket(avPacket);
}
//回撥函式 用於 播放
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {
AudioChannel *audioChannel = static_cast<AudioChannel *>(context);
//獲得pcm 資料 多少個位元組 data
int dataSize = audioChannel->getPcm();
if(dataSize > 0 ){
// 接收16位資料
(*bq)->Enqueue(bq,audioChannel->data,dataSize);
}
}
// 播放
void AudioChannel::play() {
/**
* 1、建立引擎並獲取引擎介面
*/
SLresult result;
// 1.1 建立引擎 SLObjectItf engineObject
result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
if (SL_RESULT_SUCCESS != result) {
return;
}
// 1.2 初始化引擎 init
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
return;
}
// 1.3 獲取引擎介面SLEngineItf engineInterface
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE,
&engineInterface);
if (SL_RESULT_SUCCESS != result) {
return;
}
/**
* 2、設定混音器
*/
// 2.1 建立混音器SLObjectItf outputMixObject
result = (*engineInterface)->CreateOutputMix(engineInterface, &outputMixObject, 0,
0, 0);
if (SL_RESULT_SUCCESS != result) {
return;
}
// 2.2 初始化混音器outputMixObject
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
return;
}
/**
* 3、建立播放器
*/
//3.1 配置輸入聲音資訊
//建立buffer緩衝型別的佇列 2個佇列
SLDataLocator_AndroidSimpleBufferQueue android_queue = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
2};
//pcm資料格式
//pcm+2(雙聲道)+44100(取樣率)+ 16(取樣位)+16(資料的大小)+LEFT|RIGHT(雙聲道)+小端資料
SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM, 2, SL_SAMPLINGRATE_44_1, SL_PCMSAMPLEFORMAT_FIXED_16,
SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
SL_BYTEORDER_LITTLEENDIAN};
//資料來源 將上述配置資訊放到這個資料來源中
SLDataSource slDataSource = {&android_queue, &pcm};
//3.2 配置音軌(輸出)
//設定混音器
SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
SLDataSink audioSnk = {&outputMix, NULL};
//需要的介面 操作佇列的介面
const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};
const SLboolean req[1] = {SL_BOOLEAN_TRUE};
//3.3 建立播放器
(*engineInterface)->CreateAudioPlayer(engineInterface, &bqPlayerObject, &slDataSource,
&audioSnk, 1,
ids, req);
//初始化播放器
(*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
//得到介面後呼叫 獲取Player介面
(*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerInterface);
/**
* 4、設定播放回撥函式
*/
//獲取播放器佇列介面
(*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,
&bqPlayerBufferQueueInterface);
//設定回撥
(*bqPlayerBufferQueueInterface)->RegisterCallback(bqPlayerBufferQueueInterface,
bqPlayerCallback, this);
/**
* 5、設定播放狀態
*/
(*bqPlayerInterface)->SetPlayState(bqPlayerInterface, SL_PLAYSTATE_PLAYING);
/**
* 6、手動啟用一下這個回撥
*/
bqPlayerCallback(bqPlayerBufferQueueInterface, this);
}
5.DNFFmpeg.h & DNFFmpeg.cpp
DNFFmpeg.h:
#ifndef MYFFMPEGPLAYER_DNFFMPEG_H
#define MYFFMPEGPLAYER_DNFFMPEG_H
#include "mylog.h"
#include <cstring>
#include <pthread.h>
#include "DNFFmpeg.h"
#include "JavaCallHelper.h"
#include "AudioChannel.h"
#include "VideoChannel.h"
extern "C" {
#include <libavformat/avformat.h>
}
class DNFFmpeg {
public:
DNFFmpeg(JavaCallHelper* callHelper,const char* dataSource);
~DNFFmpeg();
// 播放器準備工作
void prepare();
// 執行緒中呼叫該方法,用於實現具體解碼音視訊程式碼
void _prepare();
// 播放
void start();
// 執行緒中呼叫該方法 用於實現具體的播放程式碼
void _start();
void setRenderFrameCallback(RenderFrameCallback callback);
private:
// 音視訊地址
char *dataSource;
// 解碼執行緒
pthread_t pid;
// 解碼器上下文
AVFormatContext *formatContext = 0;
// 播放執行緒
pthread_t pid_start;
// ...
JavaCallHelper* callHelper = 0;
AudioChannel *audioChannel = 0;
VideoChannel *videoChannel = 0;
//是否正在播放
bool isPlaying = 0;
// 開始播放的回撥 解碼器解碼完成後呼叫 native_window
RenderFrameCallback callback;
};
#endif //MYFFMPEGPLAYER_DNFFMPEG_H
DNFFmpeg.cpp:
#include "DNFFmpeg.h"
// 解碼執行緒
void* task_prepare(void* args){
DNFFmpeg *dnfFmpeg = static_cast<DNFFmpeg *>(args);
//呼叫解碼方法
dnfFmpeg->_prepare();
return 0;
}
// 播放執行緒
void* task_start(void* args){
DNFFmpeg *dnfFmpeg = static_cast<DNFFmpeg *>(args);
//呼叫解碼方法
dnfFmpeg->_start();
return 0;
}
DNFFmpeg::DNFFmpeg(JavaCallHelper *callHelper, const char *dataSource){
this->callHelper = callHelper;
//防止 dataSource引數 指向的記憶體被釋放
this->dataSource = new char[strlen(dataSource)+1];
strcpy(this->dataSource,dataSource);
}
DNFFmpeg::~DNFFmpeg() {
//釋放
DELETE(dataSource);
DELETE(callHelper);
}
void DNFFmpeg::prepare() {
// 建立一個解碼的執行緒
pthread_create(&pid,NULL,task_prepare,this);
}
// 解碼器開啟的實現方法
void DNFFmpeg::_prepare() {
// 初始化網路 讓ffmpeg能夠使用網路
avformat_network_init();
//1、開啟媒體地址(檔案地址、直播地址)
// AVFormatContext 包含了 視訊的 資訊(寬、高等)
formatContext = 0;
//檔案路徑不對 手機沒網
int ret = avformat_open_input(&formatContext,dataSource,0,0);
//ret不為0表示 開啟媒體失敗
if(ret != 0){
LOGFFE("開啟媒體失敗:%s",av_err2str(ret));
callHelper->onPreError(THREAD_CHILD,FFMPEG_CAN_NOT_OPEN_URL);
return;
}
//2、查詢媒體中的 音視訊流 (給 contxt裡的 streams等成員賦)
ret = avformat_find_stream_info(formatContext,0);
// 小於0 則失敗
if (ret < 0){
LOGFFE("查詢流失敗:%s",av_err2str(ret));
callHelper->onPreError(THREAD_CHILD,FFMPEG_CAN_NOT_FIND_STREAMS);
return;
}
//nb_streams :幾個流(幾段視訊/音訊)
for (int i = 0; i < formatContext->nb_streams; ++i) {
//可能代表是一個視訊 也可能代表是一個音訊
AVStream *stream = formatContext->streams[i];
//包含了 解碼 這段流 的各種引數資訊(寬、高、位元速率、幀率)
AVCodecParameters *codecpar = stream->codecpar;
//無論視訊還是音訊都需要乾的一些事情(獲得解碼器)
// 1、通過 當前流 使用的 編碼方式,查詢解碼器
AVCodec *dec = avcodec_find_decoder(codecpar->codec_id);
if(dec == NULL){
LOGFFE("查詢解碼器失敗:%s",av_err2str(ret));
callHelper->onPreError(THREAD_CHILD,FFMPEG_FIND_DECODER_FAIL);
return;
}
//2、獲得解碼器上下文
AVCodecContext *context = avcodec_alloc_context3(dec);
if(context == NULL){
LOGFFE("建立解碼上下文失敗:%s",av_err2str(ret));
callHelper->onPreError(THREAD_CHILD,FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);
return;
}
//3、設定上下文內的一些引數 (context->width)
// context->width = codecpar->width;
// context->height = codecpar->height;
ret = avcodec_parameters_to_context(context,codecpar);
//失敗
if(ret < 0){
LOGFFE("設定解碼上下文引數失敗:%s",av_err2str(ret));
callHelper->onPreError(THREAD_CHILD,FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL);
return;
}
// 4、開啟解碼器
ret = avcodec_open2(context,dec,0);
if (ret != 0){
LOGFFE("開啟解碼器失敗:%s",av_err2str(ret));
callHelper->onPreError(THREAD_CHILD,FFMPEG_OPEN_DECODER_FAIL);
return;
}
//音訊
if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
audioChannel = new AudioChannel(i,context);
} else if(codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
videoChannel = new VideoChannel(i,context);
// 設定播放的回撥
videoChannel->setRenderFrameCallBack(callback);
}
}
//沒有音視訊 (很少見)
if(!audioChannel && !videoChannel){
LOGFFE("沒有音視訊");
callHelper->onPreError(THREAD_CHILD,FFMPEG_NOMEDIA);
return;
}
// 準備完了 通知java 你隨時可以開始播放
callHelper->onPrepare(THREAD_CHILD);
}
void DNFFmpeg::start() {
//正在播放
isPlaying = 1;
if(videoChannel){
// 設定 videoChannel工作狀態
videoChannel->start();
}
if(audioChannel){
// 設定 audioChannel工作狀態
audioChannel->start();
}
pthread_create(&pid_start,0,task_start,this);
}
void DNFFmpeg::_start() {
// 1.讀取媒體資料包(音視訊資料包)
int ret;
while(isPlaying){
// 建立一個AVPacket
AVPacket *packet = av_packet_alloc();
// 讀取流資料並塞入 AVPacket
ret = av_read_frame(formatContext,packet);
// ret == 0 成功 其他 失敗
if(ret == 0){
// 通過 packet->stream_index 判斷 是音訊還是視訊
// packet->stream_index 可以通過 解碼迴圈中存放的序號做對比
if(audioChannel && packet->stream_index == audioChannel->index){
audioChannel->packets.push(packet);
}else if(videoChannel && packet->stream_index == videoChannel->index){
videoChannel->packets.push(packet);
}
}else if(ret == AVERROR_EOF){
// 讀取完成 但是還沒有播放完
}else{
}
}
// 2.解碼
}
void DNFFmpeg::setRenderFrameCallback(RenderFrameCallback callback) {
this->callback = callback;
}
...
相關文章
- FFmpeg開發筆記(五):ffmpeg解碼的基本流程詳解(ffmpeg3新解碼api)筆記API
- FFmpeg開發筆記全目錄(FFmpeg開發實戰詳解,含直播系統的搭建過程)筆記
- ffmpeg播放器實現詳解 - 視訊顯示播放器
- oracle實驗記錄 (oracle 詳細分析redo(3))Oracle
- 教你C語言實現通訊錄的詳細程式碼C語言
- Ffmpeg視訊開發教程(一)——實現視訊格式轉換功能超詳細版
- 29.FFmpeg+OpenGLES+OpenSLES播放器實現(三.FFmpeg配置和編譯指令碼)播放器編譯指令碼
- FFmpeg程式碼實現視訊剪下
- Android 基於ffmpeg開發簡易播放器 - ffmpeg解封裝Android播放器封裝
- FFmpeg開發筆記(二十)Linux環境給FFmpeg整合AVS3解碼器筆記LinuxS3
- FFmpeg開發筆記(二十一)Windows環境給FFmpeg整合AVS3解碼器筆記WindowsS3
- asp.net 實現購物車詳細程式碼ASP.NET
- FFmpeg開發筆記(十五)詳解MediaMTX的推拉流筆記
- FFmpeg開發筆記(六)如何訪問Github下載FFmpeg原始碼筆記Github原始碼
- Android 基於ffmpeg開發簡易播放器 - NDK交叉編譯ffmpegAndroid播放器編譯
- FFmpeg開發筆記(四十)Nginx整合rtmp模組實現RTMP推拉流筆記Nginx
- 微信小程式開發記錄_01程式碼構成微信小程式
- oracle實驗記錄 (oracle 詳細分析redo(1))Oracle
- oracle實驗記錄 (oracle 詳細分析redo(2))Oracle
- oracle實驗記錄 (oracle 詳細分析redo(4))Oracle
- oracle實驗記錄 (oracle 詳細分析redo(5))Oracle
- FFMpeg SDK 開發手冊(3)
- FFmpeg開發筆記(九):ffmpeg解碼rtsp流並使用SDL同步播放筆記
- Android 音樂播放器開發實錄(MediaSession)Android播放器Session
- FFmpeg開發筆記(五十二)移動端的國產影片播放器GSYVideoPlayer筆記播放器IDE
- 11.QT-ffmpeg+QAudioOutput實現音訊播放器QT音訊播放器
- 基於FFmpeg和Qt實現簡易影片播放器QT播放器
- 基於ffmpeg的Android播放器開原始碼 Posted onAndroid播放器原始碼
- FFmpeg程式碼實現視訊轉jpg圖片
- 微信小程式開發記錄_03_開發指南_小程式程式碼組成微信小程式
- Android使用GridView實現日曆功能(詳細程式碼)AndroidView
- FFmpeg開發筆記(六十)使用國產的ijkplayer播放器觀看網路影片筆記播放器
- mysql之行(記錄)的詳細操作MySql
- FFmpeg開發筆記(十九)FFmpeg開啟兩個執行緒分別解碼音影片筆記執行緒
- FFmpeg開發筆記(八):ffmpeg解碼音訊並使用SDL同步音訊播放筆記音訊
- 小程式匯出朋友圈海報詳細記錄
- oracle實驗記錄 (oracle 10G 詳細分析undo)Oracle
- FFMpeg框架程式碼閱讀 - [3DTV]框架3D