1、環境搭建
首先需要導入所需要的包include、armeabi-v7a。
然後跟項目建立連接配接,在CMakeList.txt,并做了相關的解釋:
cmake_minimum_required(VERSION 3.4.1)
file(GLOB source_file src/main/cpp/*.cpp) //cpp檔案下所有的包
# Declares and names the project.
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${source_file})
include_directories(src/main/cpp/include)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/cpp/libs/${ANDROID_ABI}") 導入libs下的所有的包
target_link_libraries( # Specifies the target library.
native-lib
avfilter avformat avcodec avutil swresample swscale
# Links the target library to the log library
# included in the NDK.
log z android) //armeabi下的包
然後在build.gradle裡面進行配置:
ndk {
abiFilters 'armeabi-v7a'
}
然後在native-lib下導入看看能否成功。
extern "C" {
#include <libavformat/avformat.h>
}
下面正式進入視訊解碼與播放的階段:
準備階段:
首先在建立一個類,在裡面先寫好準備、開始、畫布等功能。
package com.example.player08;
import android.media.MediaPlayer;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import androidx.annotation.NonNull;
/*
提供java 進行播放 停止等操作
*/
public class DNPlayer implements SurfaceHolder.Callback {
static {
System.loadLibrary("native-lib");
}
private String dataSource;
private SurfaceHolder holder;
private OnPrepareListener listener;
/**
* 讓使用 設定播放的檔案 或者 直播位址
*/
public void setDataSource(String dataSource) {
this.dataSource = dataSource;
}
/**
* 設定播放顯示的畫布
*
* @param surfaceView
*/
public void setSurfaceView(SurfaceView surfaceView) {
holder = surfaceView.getHolder();
holder.addCallback(this);
}
public void onError(int errorCode){
System.out.println("Java接到回調:"+errorCode);
}
public void onPrepare(){
if (null != listener){
listener.onPrepare();
}
}
public void setOnPrepareListener(OnPrepareListener listener){
this.listener = listener;
}
public interface OnPrepareListener{
void onPrepare();
}
/**
* 準備好 要播放的視訊
*/
public void prepare() {
native_prepare(dataSource);
}
/**
* 開始播放
*/
public void start() {
native_start();
}
/**
* 停止播放
*/
public void stop() {
}
public void release() {
holder.removeCallback(this);
}
/**
* 畫布建立好了
*
* @param holder
*/
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
/**
* 畫布發生了變化(橫豎屏切換、按了home都會回調這個函數)
*
* @param holder
* @param format
* @param width
* @param height
*/
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
native_setSurface(holder.getSurface());
}
/**
* 銷毀畫布 (按了home/退出應用/)
*
* @param holder
*/
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
native void native_prepare(String dataSource);
}
在MainActivity裡面進行位址擷取等資訊:
package com.example.player08;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.SurfaceView;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.example.player08.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
// Used to load the 'player08' library on application startup.
private DNPlayer dnPlayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate( savedInstanceState );
setContentView( R.layout.activity_main );
SurfaceView surfaceView=findViewById( R.id.surfaceView );
dnPlayer=new DNPlayer();
dnPlayer.setSurfaceView(surfaceView);
dnPlayer.setDataSource("rtmp://47.94.57.236/myapp/");
// dnPlayer.setDataSource("rtmp://live.hkstv.hk.lxdns.com/live/hks");
dnPlayer.setOnPrepareListener(new DNPlayer.OnPrepareListener() {
@Override
public void onPrepare() {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "可以開始播放了", Toast.LENGTH_LONG).show();
}
});
dnPlayer.start();
}
});
}
public void start(View view) {
dnPlayer.prepare();
}
}
接下來開始進行c++的編寫。
相關學習資料推薦,點選下方連結免費報名,先碼住不迷路~】
【免費分享】音視訊學習資料包、大廠面試題、技術視訊和學習路線圖,資料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以點選加群免費領取~
首先,native-lib隻是一個橋梁,隻是負責傳輸資訊,然後和c++進行連接配接。
首先在native裡面建立播放器:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_player08_DNPlayer_native_1prepare(JNIEnv *env, jobject instance, jstring dataSource_) {
const char *dataSource=env->GetStringUTFChars(dataSource_,0);
//建立播放器
ffmpeg = new DNFFMPEG( dataSource);
env->ReleaseStringUTFChars(dataSource_, dataSource);
}
接下來建立DNFFMPEH.h和.cpp,視訊解碼與播放和音頻的解碼與播放主要在裡面進行。
首先native-lib傳送的資料datasource需要拷貝到DNFFMPEG中,以防止資訊被處理,傳出一個空資料。
DNFFMPEG::DNFFMPEG(JavaCallHelper *callHelper,const char *dataSource) { //構造方法
//this->dataSource=const_cast<char *>(dataSource);//不能這麼實用,因為native-lib裡面會釋放dataSource,會造成指針懸空
//防止 dataSourec參數 指向的記憶體被釋放
//strlen 獲得字元串的長度 不包括\0
this->dataSource=new char [strlen(dataSource)+1];//進行記憶體的拷貝
strcpy(this->dataSource,dataSource); //拷貝
}
DNFFMPEG::~DNFFMPEG() { //析構方法
//釋放
DELETE(dataSource);
}
接下來建立DNFFMPEH.h和.cpp,視訊解碼與播放和音頻的解碼與播放主要在裡面進行。
首先native-lib傳送的資料datasource需要拷貝到DNFFMPEG中,以防止資訊被處理,傳出一個空資料。
DNFFMPEG::DNFFMPEG(JavaCallHelper *callHelper,const char *dataSource) { //構造方法
//this->dataSource=const_cast<char *>(dataSource);//不能這麼實用,因為native-lib裡面會釋放dataSource,會造成指針懸空
//防止 dataSourec參數 指向的記憶體被釋放
//strlen 獲得字元串的長度 不包括\0
this->dataSource=new char [strlen(dataSource)+1];//進行記憶體的拷貝
strcpy(this->dataSource,dataSource); //拷貝
}
DNFFMPEG::~DNFFMPEG() { //析構方法
//釋放
DELETE(dataSource);
}
建立線程準備視訊的解碼:
void DNFFMPEG::prepare() {
//建立一個線程
pthread_create(&pid,0, task_prepare, this);
}
void* task_prepare(void *args){
DNFFMPEG *ffmpeg=static_cast<DNFFMPEG *>(args);
ffmpeg->_prepare(); //為了友善起見,防止每次調用都需要ffmpeg-> 建立有個新的線程
return 0;
}
同時在DNFFMPEG.裡面進行相應的注冊:
public:
DNFFMPEG(const char* dataSource); //接收播放的位址
~DNFFMPEG();
void prepare(); //解析datasource 位址
void _prepare();
private:
char *dataSource;
pthread_t pid;
pthread_t pid_play;
};
在解碼過程中,C++會出現報錯現象,需要傳遞給java代碼,是以需要進行java回調、簽名來講c++中的錯誤傳遞給java代碼。
在java代碼中加入onError()方法:
public void onError(int errorCode){
System.out.println("Java接到回調:"+errorCode);
}
public interface OnPrepareListener{
void onPrepare();
}
然後在cpp檔案中建立JavaCallHelper.cpp/.h來實作java的反射。
在編寫該代碼時,需要注意兩點。一個是傳遞什麼參數,為什麼傳遞該參數的問題,已經在代碼中詳細注釋了。另一個問題是需要判斷在子線程還是在主線程,在主線程可以直接使用env進行java回調,在子線程,需要借助vm進行java方法的回調,具體看代碼:
JavaCallHelper.h代碼:
//
// Created by 14452 on 2022/9/16.
//
#ifndef PLAYER08_JAVACALLHELPER_H
#define PLAYER08_JAVACALLHELPER_H
#include <jni.h>
class JavaCallHelper { //用來将c++裡面程式報錯傳給java
public:
//instance:表示反射的對象 dnplayer env:簡單調用接口函數 vm是為了跨線程
JavaCallHelper(JavaVM *vm,JNIEnv* env,jobject instance);
~JavaCallHelper();
//回調java
void onError(int thread,int errorCode); //第一個參數判斷是否在主線程還是子線程,第二個參數是錯誤資訊
void onPrepare(int thread);
private:
JavaVM *vm;
JNIEnv *env;
jobject instance;
jclass clazz;
jmethodID onErrorID;
jmethodID onPrepareID;
};
#endif //PLAYER08_JAVACALLHELPER_H
JavaCallHelper.cpp:
//
// Created by 14452 on 2022/9/16.
//
#include "JavaCallHelper.h"
#include "macro.h"
JavaCallHelper::JavaCallHelper(JavaVM *vm, JNIEnv *env, jobject instance) {
this->vm=vm;
//如果在主線程 直接進行env回調,不需要使用java vm
this->env=env;
//一旦涉及到jobject跨方法 跨線程 就需要建立全局引用
this->instance=env->NewGlobalRef(instance);
clazz=env->GetObjectClass(instance);
onErrorID=env->GetMethodID(clazz,"onError","(I)V"); //擷取java裡面onerror方法
onPrepareID=env->GetMethodID(clazz,"onPrepare","()V");
}
JavaCallHelper::~JavaCallHelper() {
env->DeleteGlobalRef(instance);
}
void JavaCallHelper::onError(int thread, int errorCode) {
//主線程
if(thread==THREAD_MAIN){
env->CallVoidMethod(instance,onErrorID,errorCode);
} else{
//子線程
JNIEnv *env;
//獲得屬于我這一個線程的jnienv
vm->AttachCurrentThread(&env,0);
env->CallVoidMethod(instance,onErrorID,errorCode);
vm->DetachCurrentThread();
}
}
void JavaCallHelper::onPrepare(int thread) {
//主線程 直接使用env
if(thread==THREAD_MAIN){
env->CallVoidMethod(instance,onPrepareID);
} else{
//子線程 需要使用 vm
JNIEnv *env;
//獲得屬于我這一個線程的jnienv
vm->AttachCurrentThread(&env,0);
env->CallVoidMethod(instance,onPrepareID);
vm->DetachCurrentThread();
}
}
在native-lib建立javaCallHelper将javaCallHelper傳遞給DNFFMEPG.cpp
JavaVM *javaVm=0;
int JNI_OnLoad(JavaVM *vm,void *r){
javaVm=vm;
return JNI_VERSION_1_6;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_player08_DNPlayer_native_1prepare(JNIEnv *env, jobject instance, jstring dataSource_) {
const char *dataSource=env->GetStringUTFChars(dataSource_,0);
//建立播放器
JavaCallHelper *helper = new JavaCallHelper(javaVm, env, instance);
ffmpeg = new DNFFMPEG(helper, dataSource);
ffmpeg->prepare();
env->ReleaseStringUTFChars(dataSource_, dataSource);
}
以上基本上實作java方法的回調。
接下來在音頻解碼個視訊解碼公用的一部分,如打開流媒體、打開編碼器等操作。
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) {
LOGE("打開媒體失敗:%s", av_err2str(ret));
callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL);
return;
}
//2、查找媒體中的 音視訊流 (給 contxt裡的 streams等成員賦)
ret = avformat_find_stream_info(formatContext, 0);
// 小于0 則失敗
if (ret < 0) {
LOGE("查找流失敗:%s", av_err2str(ret));
callHelper->onError(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) {
LOGE("查找解碼器失敗:%s", av_err2str(ret));
callHelper->onError(THREAD_CHILD, FFMPEG_FIND_DECODER_FAIL);
return;
}
//2、獲得解碼器上下文
AVCodecContext *context = avcodec_alloc_context3(dec);
if (context == NULL) {
LOGE("建立解碼上下文失敗:%s", av_err2str(ret));
callHelper->onError(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) {
LOGE("設定解碼上下文參數失敗:%s", av_err2str(ret));
callHelper->onError(THREAD_CHILD, FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL);
return;
}
// 4、打開解碼器
ret = avcodec_open2(context, dec, 0);
if (ret != 0) {
LOGE("打開解碼器失敗:%s", av_err2str(ret));
callHelper->onError(THREAD_CHILD, FFMPEG_OPEN_DECODER_FAIL);
return;
}
//機關
AVRational time_base=stream->time_base;
//音頻
if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
//0
audioChannel = new AudioChannel(i,context,time_base);
} else if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
//1
//幀率:機關時間内 需要顯示多少個圖像
AVRational frame_rate=stream->avg_frame_rate;
int fps= av_q2d(frame_rate);
videoChannel = new VideoChannel(i,context,time_base,fps);
videoChannel->setRenderFrameCallback(callback);
}
}
//沒有音視訊 (很少見)
if (!audioChannel && !videoChannel) {
LOGE("沒有音視訊");
callHelper->onError(THREAD_CHILD, FFMPEG_NOMEDIA);
return;
}
// 準備完了 通知java 你随時可以開始播放
callHelper->onPrepare(THREAD_CHILD);
};
以上資訊完成後,需要将已準備好的資訊傳遞給java層,是以需要建立prepare方法和上面error報錯的方法差不多。
為了簡化代碼,将videoChannel和AudioChannel共有的參數放進一個建立的類BaseChannel。第一個參數id,0代表音頻,1代表視訊
然後開始進行播放:
void DNFFMPEG::start() {
// 正在播放
isPlaying = 1;
// //啟動聲音的解碼與播放
if (audioChannel){
audioChannel->play();
}
if (videoChannel){
if (audioChannel){
videoChannel->play();
}
}
pthread_create(&pid_play, 0, play, this);
}
void *play(void *args) {
DNFFMPEG *ffmpeg = static_cast<DNFFMPEG *>(args);
ffmpeg->_start();
return 0;
}
/**
* 專門讀取資料包
*/
void DNFFMPEG::_start() {
//1、讀取媒體資料包(音視訊資料包)
int ret;
while (isPlaying) {
AVPacket *packet = av_packet_alloc();
ret = av_read_frame(formatContext, packet);
//=0成功 其他:失敗
if (ret == 0) {
//stream_index 這一個流的一個序号
if (audioChannel && packet->stream_index == audioChannel->id) {
audioChannel->packets.push(packet);
}
if (videoChannel && packet->stream_index == videoChannel->id) {
videoChannel->packets.push(packet);
}
} else if (ret == AVERROR_EOF) {
//讀取完成 但是可能還沒播放完
} else {
//
}
}
};
packet申請的記憶體在堆中,需要釋放記憶體,且packet參數公用在音頻和視訊的解碼中,是以在baseChannel裡面進行記憶體釋放。
/**
* 釋放 AVPacket
* @param packet
*/
static void releaseAvPacket(AVPacket** packet) {
if (packet) {
av_packet_free(packet);
//為什麼用指針的指針?
// 指針的指針能夠修改傳遞進來的指針的指向
*packet = 0;
}
}
解碼:取出資料包->将包丢給解碼器->從解碼器中讀取 解碼後的資料包
播放(目标是先将資料包轉換成RGBA,通過sws_scale進行轉換,然後在ANativeWindow裡面進行畫畫。(注意:要是用同步鎖,防止在畫畫過程中被釋放)
解碼:
void VideoChannel::play() {
isPlaying = 1;
frames.setWork(1);
packets.setWork(1);
//1、解碼
pthread_create(&pid_decode, 0, decode_task, this);
//2、播放
pthread_create(&pid_render, 0, render_task, this);
}
void *decode_task(void *args) {
VideoChannel *channel = static_cast<VideoChannel *>(args);
channel->decode();
return 0;
}
//解碼
void VideoChannel::decode() {
AVPacket *packet = 0;
while (isPlaying) {
//取出一個資料包
int ret = packets.pop(packet);
if (!isPlaying) {
break;
}
//取出失敗
if (!ret) {
continue;
}
//把包丢給解碼器
ret = avcodec_send_packet(avCodecContext, packet);
releaseAvPacket(&packet);
//重試
if (ret != 0) {
break;
}
//代表了一個圖像 (将這個圖像先輸出來)
AVFrame *frame = av_frame_alloc();
//從解碼器中讀取 解碼後的資料包 AVFrame
ret = avcodec_receive_frame(avCodecContext, frame);
//需要更多的資料才能夠進行解碼
if (ret == AVERROR(EAGAIN)) {
continue;
} else if(ret != 0){
break;
}
//再開一個線程 來播放 (流暢度)
frames.push(frame);
}
releaseAvPacket(&packet);
}
播放:
void VideoChannel::play() {
isPlaying = 1;
frames.setWork(1);
packets.setWork(1);
//1、解碼
pthread_create(&pid_decode, 0, decode_task, this);
//2、播放
pthread_create(&pid_render, 0, render_task, this);
}
void *render_task(void *args) {
VideoChannel *channel = static_cast<VideoChannel *>(args);
channel->render();
return 0;
}
//播放
void VideoChannel::render() {
//目标: RGBA
swsContext = sws_getContext(
avCodecContext->width, avCodecContext->height,avCodecContext->pix_fmt,
avCodecContext->width, avCodecContext->height,AV_PIX_FMT_RGBA,
SWS_BILINEAR,0,0,0);
AVFrame* frame = 0;
//指針數組
uint8_t *dst_data[4];
int dst_linesize[4];
av_image_alloc(dst_data, dst_linesize,
avCodecContext->width, avCodecContext->height,AV_PIX_FMT_RGBA, 1);
while (isPlaying){
int ret = frames.pop(frame);
if (!isPlaying){
break;
}
//src_linesize: 表示每一行存放的 位元組長度
sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),
frame->linesize, 0,
avCodecContext->height,
dst_data,
dst_linesize);
//回調出去進行播放
callback(dst_data[0],dst_linesize[0],avCodecContext->width, avCodecContext->height);
releaseAvFrame(&frame);
}
av_freep(&dst_data[0]);
releaseAvFrame(&frame);
}
在native-lib中畫畫:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ;
//畫畫
void render(uint8_t *data, int lineszie, int w, int h) {
pthread_mutex_lock(&mutex);
if (!window) {
pthread_mutex_unlock(&mutex);
return;
}
//設定視窗屬性
ANativeWindow_setBuffersGeometry(window, w,
h,
WINDOW_FORMAT_RGBA_8888);
ANativeWindow_Buffer window_buffer;
if (ANativeWindow_lock(window, &window_buffer, 0)) {
ANativeWindow_release(window);
window = 0;
pthread_mutex_unlock(&mutex);
return;
}
//填充rgb資料給dst_data
uint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits);
// stride:一行多少個資料(RGBA) *4
int dst_linesize = window_buffer.stride * 4;
//一行一行的拷貝
for (int i = 0; i < window_buffer.height; ++i) {
//memcpy(dst_data , data, dst_linesize);
memcpy(dst_data + i * dst_linesize, data + i * lineszie, dst_linesize);
}
ANativeWindow_unlockAndPost(window);
pthread_mutex_unlock(&mutex);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_player08_DNPlayer_native_1setSurface(JNIEnv *env, jobject instance, jobject surface) {
pthread_mutex_lock(&mutex);
if (window){ //判斷之前是否有surface
//把老的釋放
ANativeWindow_release(window);
window=0;
}
window=ANativeWindow_fromSurface(env,surface);
pthread_mutex_unlock(&mutex);
}
然後采用EV錄屏進行線上播放:
連結:https://pan.baidu.com/s/1au6zAAa7-Fdh6uNggPTRig
提取碼:j7qn
原文 ffmpeg播放器(一) 視訊解碼與播放