最近做了一个支持onvif标准的摄像头管理程序,主要就是在局域网中通过程序控制摄像头获取摄像头视频流分发给广域网中的客户端设备;这里面用了onvif协议相关知识和ffmpeg视频流处理的相关知识。
onvif相关的以后有机会再分享,今天主要是把android ffmpeg相关的东西分享一下。
视频播放流程:
常见封装格式有mp4、3gp、AVI等,这些封装格式包含了音视频流相关信息。每种封装格式都有自己的规则那么解封装就是按照规则分离出音视频数据。
例如:mp4格式
MP4(MPEG-4 Part 14)是一种常见的多媒体容器格式,它是在“ISO/IEC 14496-14”标准文件中定义的,属于MPEG-4的一部分,是“ISO/IEC 14496-12(MPEG-4 Part 12 ISO base media file format)”标准中所定义的媒体格式的一种实现,后者定义了一种通用的媒体文件结构标准。MP4是一种描述较为全面的容器格式,被认为可以在其中嵌入任何形式的数据,各种编码的视频、音频等都不在话下,不过我们常见的大部分的MP4文件存放的AVC(H.264)或MPEG-4(Part 2)编码的视频和AAC编码的音频。MP4格式的官方文件后缀名是“.mp4”,格式规则是由一个个“box”组成的,大box中存放小box,一级嵌套一级来存放媒体信息。
由于原始音视频数据相当大,如果想在网络中传输就会不可行,那么视频音频压缩就解决这个问题,常见的视频压缩格式有h264、h265;音频常见压缩格式有aac、MP3;那么解码解释按照对应的压缩格式解析出原始音视频数据。
播放原始音频、视频数据pcm、 rgba或者yuv,播放一般针对平台使用api进行渲染和播放。
我做的IPC摄像头管理程序中用到ffmpeg相关的点有:
在局域网中获取IPC的rtp视频流–>解封装获取视频和音频流数据–>通过p2p点到点广域网通讯(基于ice服务)将数据分发给客户端–>在客户端进行音视频流解码–>通过android的api进行pcm播放和rgba渲染。
关于p2p这块以后有机会分享。
@Keep
public native long initVideo(String path);//初始化读取流信息包括本地文件或者是rtmp、rtsp、http等、支持直播。
@Keep
public native int playVideo(long i, int handleType, int isCloseDelay, int handleTypeA, int catTime);//每次读取一帧数据(音频则是一段采样数据)
@Keep
public native void videoSeek(long i, int time);//如果是文件而不是直播可以切换进度,单位是秒
@Keep
public native void stopPlayer(long i);//停止释放流读取
@Keep
public native long initAudioDecode(String cName,int rate, int chs, String fmt);//初始化软解音频解码器
@Keep
public native int destroyAudioDecode(long i);//释放软件解码器
@Keep
public native int sendAudioData(long i,byte[] arr, int len);//向解码器推送数据
@Keep
public native long initVideoDecode(String codec, Surface surface, float delayTime);//初始化软解视频解码器
@Keep
public native int destroyVideoDecode(long i);//释放软件解码器
@Keep
public native int sendVideoData(long i, byte[] arr, int len, int isCloseDelay, int isSendOut, Surface surface);//向解码器推送数据
上面的接口都是通过ffmpeg封装而成jni接口可以当做工具类使用。
由于每个流程都是分离的如果软解不能够满足你的需要可以使用硬解码。
使用硬件驱动进行解码。
软件使用的ffmpeg的格式库兼容格式多,性能方面差手机容易放热。使用CPU进行编码,如常见C/C++代码,编译生成二进制文件,速度相对较慢。
硬解码性能好、手机不容易发热,兼容格式类型少(相对而言)。实际上调用的是底层的高清编码硬件模块,即显卡,不使用CPU,速度快。
首先看看音频播放,这里已经将功能封装成一个工具累了通过android audiotrack 进行播放,可以播放pcm的类型有fltp、16s、8s这三种类型。
package com.liyihang.jason;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Build;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class AudioPlayer{
private int fmt;
private int rate;
private int chs;
private AudioTrack audioTrack;
private int bLen;
public AudioPlayer(int rate, int chs, String fmts) {
this.rate=rate;
this.chs=chs;
int channels = chs==1?AudioFormat.CHANNEL_OUT_MONO:AudioFormat.CHANNEL_OUT_STEREO;
fmt = AudioFormat.ENCODING_PCM_8BIT;
bLen=1;
if (fmts.contains("fltp")){
fmt = AudioFormat.ENCODING_PCM_FLOAT;
bLen=4;
}else if (fmts.contains("s16")){
fmt = AudioFormat.ENCODING_PCM_16BIT;
bLen=2;
}
int bufferSize = AudioTrack.getMinBufferSize(rate, channels, fmt);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
audioTrack = new AudioTrack.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build())
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(fmt)
.setSampleRate(rate)
.setChannelMask(channels)
.build())
.setTransferMode(AudioTrack.MODE_STREAM)
.setBufferSizeInBytes(bufferSize)
.build();
} else {
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, rate, channels, fmt,
bufferSize, AudioTrack.MODE_STREAM);
}
audioTrack.play();
}
public int getByteLen() {
return bLen;
}
public int getRate() {
return rate;
}
public int getChs() {
return chs;
}
public void write(final byte[] arr, final int len){
if (audioTrack==null)
return;
if (fmt==AudioFormat.ENCODING_PCM_FLOAT){
float[] floats = bytesToFloats(arr, arr.length, false);
audioTrack.write(floats, 0, floats.length, AudioTrack.WRITE_BLOCKING);
} else {
audioTrack.write(arr, 0, arr.length);
}
}
public void close(){
if (audioTrack!=null){
try {
audioTrack.pause();
audioTrack.flush();
audioTrack.stop();
audioTrack.release();
audioTrack=null;
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 将byte[] 数组转换成float[]数组
public static float[] bytesToFloats(byte[] bytes,int len,boolean isBe) {
if(bytes==null){
return null;
}
float[] floats = new float[len/4];
// 大端序
if (isBe) {
ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asFloatBuffer().get(floats);
} else {
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().get(floats);
}
return floats;
}
}
构造函数需要的参数有:
rate:音频采样率
chs:声道数
fmt:pcm格式 支持三种
视频的渲染主要是通过surfaceview来完成,主要是获取surface来配合渲染:
java层只是获取surface指针,主要操作在c层,所以java代码很简单就是监听声明周期来监听surface的产生。
package com.liyihang.jason;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.util.concurrent.atomic.AtomicBoolean;
public class SurfaceViewMediaPlayer extends SurfaceView implements SurfaceHolder.Callback , MediaCodecUtils.OnMediaPlayerListener {
public static final int surface_create=457;
public static final int surface_free=458;
public static final int progress_bar=459;
public static final int error_end=460;
public static final int size_change=461;
private AtomicBoolean isRuns=new AtomicBoolean(false);
private Handler.Callback callback;
private MediaCodecUtils mediaCodecUtils;
public SurfaceViewMediaPlayer(Context context) {
super(context);
init();
}
public SurfaceViewMediaPlayer(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SurfaceViewMediaPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
getHolder().addCallback(this);
}
public void setCallback(Handler.Callback callback) {
this.callback = callback;
}
public boolean isRuns() {
return isRuns.get();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
isRuns.set(true);
if (mediaCodecUtils!=null){
mediaCodecUtils.free();
}
if (callback!=null)
callback.handleMessage(msgObj(surface_create,null));
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (callback!=null)
callback.handleMessage(msgObj(surface_free,null));
closeMedia();
isRuns.set(false);
}
private void closeMedia() {
if (mediaCodecUtils!=null){
mediaCodecUtils.free();
mediaCodecUtils=null;
}
}
public static Message msgObj(int what, Object object) {
Message obtain = Message.obtain();
obtain.obj = object;
obtain.what = what;
return obtain;
}
public void startPlay(String url){
if (!isRuns.get())
return;
closeMedia();
mediaCodecUtils=new MediaCodecUtils(url, this);
mediaCodecUtils.startPlayer();
}
public void setPause(boolean isPause){
if (mediaCodecUtils!=null)
mediaCodecUtils.setIsPause(isPause);
}
public void seek(int time){
//秒钟
if (mediaCodecUtils!=null)
mediaCodecUtils.setSeek(time);
}
@Override
public Surface onGetSurface() {
return isRuns.get()? getHolder().getSurface() : null;
}
@Override
public void onErrorEnd(int i) {
if (callback!=null)
callback.handleMessage(msgObj(error_end,null));
}
@Override
public void onViewSizeChange(int w, int h) {
Message message = msgObj(size_change, null);
message.arg1=w;
message.arg2=h;
if (callback!=null)
callback.handleMessage(message);
}
@Override
public void onProgress(int now, int allTime) {
Message message = msgObj(progress_bar, null);
message.arg1=now;
message.arg2=allTime;
if (callback!=null)
callback.handleMessage(message);
}
}
最终代码封装成一个SurfaceViewMediaPlayer 使用起来非常方便
package com.jasonliyihang.ffmpegvideoplayer;
import android.Manifest;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.liyihang.jason.MediaCodecUtils;
import com.liyihang.jason.SurfaceViewMediaPlayer;
import java.util.List;
import pub.devrel.easypermissions.EasyPermissions;
public class MainActivity extends AppCompatActivity implements View.OnClickListener, Handler.Callback {
private SurfaceViewMediaPlayer surfaceView;
private ProgressBar progressBar;
private FrameLayout frameLayout;
private TextView allTimeTxt;
private TextView nowTimeTxt;
private String url;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
surfaceView=findViewById(R.id.surfaceView);
progressBar=findViewById(R.id.progressBar);
frameLayout=findViewById(R.id.showLayout);
allTimeTxt=findViewById(R.id.allTime);
nowTimeTxt=findViewById(R.id.nowTime);
surfaceView.setCallback(this);
// todo 请设置你的播放地址 可是本地地址 或者在线地址 支持rtmp rtsp http udp 等多种协议
url=Environment.getExternalStorageDirectory().getAbsolutePath()+"/bs2.mp4";
checkPerm();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, new EasyPermissions.PermissionCallbacks() {
@Override
public void onPermissionsGranted(int requestCode, @NonNull List<String> perms) {
checkPerm();
}
@Override
public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
finish();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
}
});
}
public static final int WRITE_EXTERNAL_STORAGE = 100;
private void checkPerm() {
String[] params = {
Manifest.permission.WRITE_EXTERNAL_STORAGE};
if (EasyPermissions.hasPermissions(this, params)) {
} else {
EasyPermissions.requestPermissions(this, "需要读写本地权限", WRITE_EXTERNAL_STORAGE, params);
}
}
@Override
public void onClick(View v) {
if (v.getId()==R.id.pauseBtn){
surfaceView.setPause(true);
}
if (v.getId()==R.id.pauseBtn2){
surfaceView.setPause(false);
}
if (v.getId()==R.id.pauseBtn3){
surfaceView.seek(50);//秒
}
if (v.getId()==R.id.pauseBtn4){
surfaceView.startPlay("rtmp://202.69.69.180:443/webcast/bshdlive-pc");
}
}
@Override
public boolean handleMessage(@NonNull final Message msg) {
if (msg.what==SurfaceViewMediaPlayer.surface_create){
surfaceView.startPlay(url);
}
if (msg.what==SurfaceViewMediaPlayer.surface_free){
}
if (msg.what==SurfaceViewMediaPlayer.error_end){
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "异常退出", Toast.LENGTH_LONG).show();
}
});
}
if (msg.what==SurfaceViewMediaPlayer.size_change){
runOnUiThread(new Runnable() {
@Override
public void run() {
ViewGroup.LayoutParams layoutParams = frameLayout.getLayoutParams();
layoutParams.width=msg.arg1;//画面宽度
layoutParams.height=msg.arg2;//画面的高度
frameLayout.setLayoutParams(layoutParams);
}
});
}
if (msg.what==SurfaceViewMediaPlayer.progress_bar){
runOnUiThread(new Runnable() {
@Override
public void run() {
progressBar.setMax(msg.arg2);//全部时间
progressBar.setProgress(msg.arg1);//当前进度
nowTimeTxt.setText(MediaCodecUtils.generateTime(msg.arg1*1000));
allTimeTxt.setText(MediaCodecUtils.generateTime(msg.arg2*1000));
}
});
}
return false;
}
}
一个视频播放器具备知识节点有媒体格式、编解码、多线程调度、时序管理、平台api等方面。虽然现在很多的视频播放器都可以现成使用,但是自己造半个轮子会对多媒体方面了解更加深刻一些;为什么说是半个轮子,因为底层协议、文件格式、ffmpeg、android音视频api才是精华所在,也可以展开深入的研究。
项目虽然说是一个播放器但是每个每个层面都是分开,例如可以做成p2p点到点媒体的传输和播放;每个api都可以独立当做工具类使用。
项目地址:https://download.csdn.net/download/mhhyoucom/12837529