ffmpeg学习笔记-native原生绘制
上次已将ffmpeg的动态库编译出来了,并且使用了ffmpeg的转码功能,成功将mp4格式视频转化为yuv视频,这篇文章基于上次测试的demo,使用surfaceview显示解码完成的像素数据
布局设置和权限添加
布局
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.cj5785.ffmpegnativeplayer.view.MySurfaceView
android:id="@+id/surface_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始"
android:onClick="mPlay" />
</FrameLayout>
权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
编写自定义view和控制器
自定义View
package com.cj5785.ffmpegnativeplayer.view;
import android.content.Context;
import android.graphics.PixelFormat;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class MySurfaceView extends SurfaceView {
public MySurfaceView(Context context) {
super(context);
init();
}
public MySurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MySurfaceView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
//初始化像素格式
private void init() {
SurfaceHolder holder = getHolder();
holder.setFormat(PixelFormat.RGBA_8888);
}
}
控制器
package com.cj5785.ffmpegnativeplayer;
import android.view.Surface;
public class NativePlayer {
public native void render(String input, Surface surface);
static {
System.loadLibrary("avutil-54");
System.loadLibrary("swresample-1");
System.loadLibrary("avcodec-56");
System.loadLibrary("avformat-56");
System.loadLibrary("swscale-3");
System.loadLibrary("postproc-53");
System.loadLibrary("avfilter-5");
System.loadLibrary("avdevice-56");
System.loadLibrary("ffmpeg_native_player");
}
}
实现控制器native方法
使用javah生成头文件,这里可能存在无法找到Surface签名的问题,这时候需要指定classpath路径
javah -classpath E:\eclipse-adt\sdk\platforms\android-15\android.jar;. com.cj5785.ffmpegnativeplayer.NativePlayer
格式说明:-classpath
后面跟的是android.jar
路径,最后接native方法类的全名新建jni文件夹,将头文件移至jni文件夹,添加本地依赖
复制生成ffmpeg的include目录和so动态库到jni目录
将之前的
Android.mk
和Application.mk
复制到jni文件夹,并做适当修改
Android.mk
主要修改模块名,使其与控制器调用相统一
Application.mk
主要将APP_PLATFORM := android-8
修改为APP_PLATFORM := android-9
注意,此处如果不修改Application.mk
将导致android/native_window_jni.h
无法找到,同时,由于使用了这个头文件,需要在Android.mk
配置-landroid
使用开源库libyuv实现yuv转化为RGBA_8888
下载开源库libyuv,下载地址libyuv下载地址
将libyuv下的所有文件放入jni目录(NDK工程规范,必须存在jni目录)
修改libyuv的Android.mk文件,将最后的include $(BUILD_STATIC_LIBRARY)
改为include $(BUILD_SHARED_LIBRARY)
,这样就可以生成so动态库了
还可以将LOCAL_MODULE := libyuv_static
改为LOCAL_MODULE := libyuv
,方便so管理
在jni目录下执行ndk-build
即可对libyuv进行编译
编译生成的so动态库位于与jni目录同级的lib下
将lib添加到工程jni目录下,为了便于管理,将jni的include目录进行重新分配,重新分配目录如下:(已将libyuv的include加入到工程,这里没有列出目录下包含的头文件)
│ Android.mk
│ Application.mk
│ com_cj5785_ffmpegnativeplayer_NativePlayer.h
│ ffmpeg_native_player.c
│
└─include
├─ffmpeg
│ │ libavcodec-56.so
│ │ libavdevice-56.so
│ │ libavfilter-5.so
│ │ libavformat-56.so
│ │ libavutil-54.so
│ │ libpostproc-53.so
│ │ libswresample-1.so
│ │ libswscale-3.so
│ │
│ ├─libavcodec
│ ├─libavdevice
│ ├─libavfilter
│ ├─libavformat
│ ├─libavutil
│ ├─libpostproc
│ ├─libswresample
│ └─libswscale
│
└─libyuv
│ libyuv.h
│ libyuv.so
│
└─libyuv
- 修改
Android.mk
,使其能找到so动态库
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := avcodec
LOCAL_SRC_FILES := include/ffmpeg/libavcodec-56.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := avdevice
LOCAL_SRC_FILES := include/ffmpeg/libavdevice-56.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := avfilter
LOCAL_SRC_FILES := include/ffmpeg/libavfilter-5.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := avformat
LOCAL_SRC_FILES := include/ffmpeg/libavformat-56.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := avutil
LOCAL_SRC_FILES := include/ffmpeg/libavutil-54.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := postproc
LOCAL_SRC_FILES := include/ffmpeg/libpostproc-53.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := swresample
LOCAL_SRC_FILES := include/ffmpeg/libswresample-1.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := swscale
LOCAL_SRC_FILES := include/ffmpeg/libswscale-3.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := yuv
LOCAL_SRC_FILES := include/libyuv/libyuv.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg_native_player
LOCAL_SRC_FILES := ffmpeg_native_player.c
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include/ffmpeg
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include/libyuv
LOCAL_LDLIBS := -llog -landroid
LOCAL_SHARED_LIBRARIES := avcodec avdevice avfilter avformat avutil postproc swresample swscale yuv
include $(BUILD_SHARED_LIBRARY)
- 修改
Application.mk
,更改APP_PLATFORM
,使其可以使用android/native_window_jni.h
和android/native_window.h
头文件
APP_ABI := armeabi armeabi-v7a
APP_PLATFORM := android-9
- 实现jni头文件声明的函数
#include <android/log.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include "com_cj5785_ffmpegnativeplayer_NativePlayer.h"
//封装格式
#include "include/ffmpeg/libavformat/avformat.h"
//解码
#include "include/ffmpeg/libavcodec/avcodec.h"
//像素处理
#include "include/ffmpeg/libswscale/swscale.h"
//包含yuvlib头文件
#include "include/libyuv/libyuv.h"
#define LOGI(FORMAT,...) __android_log_print(4,"cj5785",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT,...) __android_log_print(6,"cj5785",FORMAT,##__VA_ARGS__);
JNIEXPORT void JNICALL Java_com_cj5785_ffmpegnativeplayer_NativePlayer_render
(JNIEnv *env, jobject jobj, jstring jstr_path, jobject obj_surface)
{
LOGE("%s", "开始");
const char *input_cstr = (*env)->GetStringUTFChars(env, jstr_path, NULL);
//1.注册组件
av_register_all();
AVFormatContext *pFormatCtx = avformat_alloc_context();
//2.打开视频文件
if(avformat_open_input(&pFormatCtx, input_cstr, NULL, NULL) != 0)
{
LOGE("%s", "打开文件失败!");
return;
}
//3.获取视频相关信息
if(avformat_find_stream_info(pFormatCtx, NULL) < 0)
{
LOGE("%s", "获取视频信息失败!");
return;
}
int i = 0;
int video_stream_index = -1;
for (i = 0; i < pFormatCtx->nb_streams; i++) {
if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
video_stream_index = i;
break;
}
}
if (video_stream_index == -1)
{
LOGE("%s","找不到视频流\n");
return;
}
//4.获取解码器
AVCodecContext *pCodecCtx = pFormatCtx->streams[video_stream_index]->codec;
AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec == NULL)
{
LOGE("%s", "无法解码!");
return;
}
//5.打开解码器
if(avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
{
LOGE("%s", "解码失败!");
return;
}
//6.以帧为单位读取视频文件
AVPacket *packet = (AVPacket *)av_malloc(sizeof(AVPacket));
AVFrame *pFrame = av_frame_alloc();
AVFrame *pRGBFrame = av_frame_alloc();
//native绘制
//窗体设置
ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, obj_surface);
//缓冲区设置
ANativeWindow_Buffer outBuffer;
int len, got_frame, frame_count = 0;
while(av_read_frame(pFormatCtx, packet) >= 0)
{
if(packet->stream_index == video_stream_index)
{
len = avcodec_decode_video2(pCodecCtx, pFrame, &got_frame, packet);
if(len < 0)
{
LOGE("%s","解码错误!");
return;
}
if(got_frame)
{
LOGI("解码第%d帧", frame_count++);
//a.lock
//设置缓冲区属性(宽,高,像素格式)
ANativeWindow_setBuffersGeometry(nativeWindow, pCodecCtx->width, pCodecCtx->height, WINDOW_FORMAT_RGBA_8888);
ANativeWindow_lock(nativeWindow, &outBuffer, NULL);
//b.fix buffer
//设置RGB的缓冲区以及属性(像素格式,宽高),RGB缓冲区和outBuffer.bits是同一块内存
avpicture_fill((AVPicture *)pRGBFrame, outBuffer.bits, AV_PIX_FMT_RGBA, pCodecCtx->width, pCodecCtx->height);
//YUV转化为RGB
I420ToARGB(pFrame->data[0], pFrame->linesize[0],
pFrame->data[2], pFrame->linesize[2],
pFrame->data[1], pFrame->linesize[1],
pRGBFrame->data[0], pRGBFrame->linesize[0],
pCodecCtx->width, pCodecCtx->height);
//c.unlock
ANativeWindow_unlockAndPost(nativeWindow);
usleep(16 * 1000);
}
}
av_free_packet(packet);
}
ANativeWindow_release(nativeWindow);
av_frame_free(&pFrame);
av_frame_free(&pRGBFrame);
avcodec_close(pCodecCtx);
avformat_free_context(pFormatCtx);
(*env)->ReleaseStringUTFChars(env, jstr_path, input_cstr);
}
调用native,使其能够播放
package com.cj5785.ffmpegnativeplayer;
import java.io.File;
import com.cj5785.ffmpegnativeplayer.view.MySurfaceView;
import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.view.Surface;
import android.view.View;
public class MainActivity extends Activity {
private NativePlayer player;
private MySurfaceView mySurfaceView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mySurfaceView = (MySurfaceView) findViewById(R.id.surface_view);
player = new NativePlayer();
}
public void mPlay(View view) {
String input = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar + "oneplus.mp4";
Surface surface = mySurfaceView.getHolder().getSurface();
player.render(input, surface);
}
}
至此,已经可以编译生成apk了,在手机上测试也没有问题
更改布局和主活动,使其可以播放多个测试视频
MainActivity.java
package com.cj5785.ffmpegnativeplayer;
import java.io.File;
import com.cj5785.ffmpegnativeplayer.view.MySurfaceView;
import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.view.Surface;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
public class MainActivity extends Activity {
private NativePlayer player;
private MySurfaceView mySurfaceView;
private Spinner sp_video;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mySurfaceView = (MySurfaceView) findViewById(R.id.surface_view);
sp_video = (Spinner)findViewById(R.id.sp_video);
player = new NativePlayer();
//视频列表
String[] videoArray = getResources().getStringArray(R.array.video_list);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, android.R.id.text1,videoArray);
sp_video.setAdapter(adapter);
}
public void mPlay(View view) {
String filename = sp_video.getSelectedItem().toString();
String input = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar + filename;
Surface surface = mySurfaceView.getHolder().getSurface();
player.render(input, surface);
}
}
activity_main.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.cj5785.ffmpegnativeplayer.view.MySurfaceView
android:id="@+id/surface_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Spinner
android:id="@+id/sp_video"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始"
android:onClick="mPlay" />
</LinearLayout>
</FrameLayout>
在string.xml
中添加数组值
<string-array name="video_list">
<item>naxienian.mp4</item>
<item>cuc_ieschool.mkv</item>
<item>sintel.wmv</item>
<item>Nocturne.m4a</item>
</string-array>
需要注意的问题
在native的实现过程中,I420ToARGB()方法在调用的时候,UV的位置是颠倒的,需要对调UV的位置
在这个示例程序中,旨在说明native是怎么绘制的,其代码存在严重不足,比如在主线程中绘制界面
部分视频会出现花屏现象,这个问题在后面多线程解码的时候会解决
ffmpeg学习笔记-native原生绘制的更多相关文章
- Cocos2d-x 学习笔记(25) 渲染 绘制 Render
[Cocos2d-x]学习笔记目录 本文链接:https://www.cnblogs.com/deepcho/p/cocos2dx-render.html 1. 从程序入口到渲染方法 一个Cocos2 ...
- ffmpeg学习笔记-多线程音视频解码
之前的视频解码仍然存在问题,那就是是在主线程中去完成解码的,会造成线程阻塞,这里将其改为多线程解码,使其主线程不被阻塞 前面介绍了音视频的主线程解码,那样会阻塞主线程,在前面学习了多线程以后,就可以对 ...
- ffmpeg学习笔记
对于每一个刚開始学习的人,刚開始接触ffmpeg时,想必会有三个问题最为关心,即ffmpeg是什么?能干什么?怎么開始学习?本人前段时间開始接触ffmpeg,在刚開始学习过程中.这三个问 ...
- 【canvas学习笔记二】绘制图形
上一篇我们已经讲述了canvas的基本用法,学会了构建canvas环境.现在我们就来学习绘制一些基本图形. 坐标 canvas的坐标原点在左上角,从左到右X轴坐标增加,从上到下Y轴坐标增加.坐标的一个 ...
- ffmpeg学习笔记-音频播放
前文讲到音频解码,将音频解码,并且输入到PCM文件,这里将音频通过AudioTrack直接输出 音频播放说明 在Android中自带的MediaPlayer也可以对音频播放,但其支持格式太少 使用ff ...
- ffmpeg学习笔记-音频解码
在之前的文章已经初步对视频解码有个初步的认识了,接下来来看一看音频解码 音频解码步骤 音频解码与视频解码一样,有者固有的步骤,只要按照步骤来,就能顺利的解码音频 以上是ffmpeg的解码流程图,可以看 ...
- ffmpeg学习笔记-Linux下编译Android动态库
Android平台要使用ffmpeg就需要编译生成动态库,这里采用Ubuntu编译Android动态库 文件准备 要编译生成Android需要以下文件 NDK ffmpeg源代码 NDK下载 NDK可 ...
- ffmpeg学习笔记-初识ffmpeg
ffmpeg用来对音视频进行处理,那么在使用ffmpeg前就需要ffmpeg有一个大概的了解,这里使用雷神的ppt素材进行整理,以便于复习 音视频基础知识 视频播放器的原理 播放视频的流程大致如下: ...
- 【canvas学习笔记四】绘制文字
本节我们来学习如何绘制文字. 绘制文字有两个主要的方法: fillText(text, x, y [, maxWidth]) 在x, y位置填充文字text,有一个可选参数maxWidth设置最大绘制 ...
随机推荐
- 1、课程简介-Spring 注解驱动开发
1.课程简介-Spring 注解驱动开发
- python自动华 (四)
Python自动化 [第四篇]:Python基础-装饰器 生成器 迭代器 Json & pickle 目录: 装饰器 生成器 迭代器 Json & pickle 数据序列化 软件目录结 ...
- PHP实现页面跳转功能
PHP跳转到指定页面的问题通常都会建设网站需求上看到,比如我们需要从一个页面跳转到另一个页面来实现某个功能或者效果.其实在PHP中进行页面跳转是有多种方法的,那么这篇文章就给大家介绍下,有哪些方法可以 ...
- 017_STM32程序移植之_AS608指纹模块
STM32程序移植之AS608指纹模块 BUG说明: 硬件接线图如图所示 STM32引脚 指纹模块引脚 功能 3.3V 3.3V PA3 Tx PA2 Rx GND GND PA1 WAK 3.3V ...
- js字符串字母大小写转换
toLocaleUpperCase 方法 返回一个字符串,其中所有的字母字符都被转换为大写,同时适应宿主环境的当前区域设置. stringVar.tolocaleUpperCase( )必选的 str ...
- BigDecimal 3个toString()方法区别
BigDecimal 的toEngineeringString.toPlainString和toString方法的区别: toEngineeringString:有必要时使用工程计数法.工程记数法是一 ...
- date/clock/hwclock/cal
date 显示日期与时间 date +%Y/%m/%d/%H:%M 2018/10/08/17:35 格式化输出 时间的设置 查看时区 date -R 时间戳转化 time1=$(date +%s - ...
- Ubuntu 16.04 下Redis Cluster集群搭建
实际操作如下: 准备工作 版本:4.0.2 下载地址:https://redis.io/download 离线版本:(链接: https://pan.baidu.com/s/1bpwDtOr 密码: ...
- mac安装需要的骚操作
显示隐藏文件 defaults write com.apple.finder AppleShowAllFiles -bool true; KillAll Finder 允许任何来源 sudo spct ...
- AcWing:241. 楼兰图腾(树状数组逆序对)
在完成了分配任务之后,西部314来到了楼兰古城的西部. 相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(‘V’),一个部落崇拜铁锹(‘∧’),他们分别用V和∧的形状来代表各自 ...