一、基本概念
- 编码
编码是信息从一种形式或格式转换为另一种形式的过程。用预先规定的方法将文字、数字或其他对象编成数码,或将信息、数据转换成规定的电脉冲信号。在本模块中,编码是指编码器将原始的视频信息压缩为另一种格式的过程。
- 解码
解码是一种用特定方法,把数码还原成它所代表的内容或将电脉冲信号、光信号、无线电波等转换成它所代表的信息、数据等的过程。在本模块中,解码是指解码器将接收到的数据还原为视频信息的过程,与编码过程相对应。
- 帧率
帧率是以帧称为单位的位图图像连续出现在显示器上的频率(速率),以赫兹(Hz
)为单位。该术语同样适用于胶片、摄像机、计算机图形和动作捕捉系统。
二、常用API
1、媒体编解码能力查询
CodecDescriptionList :提供设备可用的编解码器支持的媒体格式的MIME
类型列表。您可以使用此类中的方法来检查设备是否支持指定MIME
类型的编解码器
接口名 | 功能描述 |
---|---|
getSupportedMimes() | 获取某设备所支持的编解码器的MIME列表。 |
isDecodeSupportedByMime(String mime) | 判断某设备是否支持指定MIME对应的解码器。 |
isEncodeSupportedByMime(String mime) | 判断某设备是否支持指定MIME对应的编码器。 |
isDecoderSupportedByFormat(Format format) | 判断某设备是否支持指定媒体格式对应的解码器。 |
isEncoderSupportedByFormat(Format format) | 判断某设备是否支持指定媒体格式对应的编码器。 |
获取某设备所支持的编解码器的
MIME
列表
List<String> supportedMimes = CodecDescriptionList.getSupportedMimes();
for (String supportedMime : supportedMimes) {
HiLog.info(hiLogLabel,supportedMime);
}
判断某设备是否支持指定
MIME
对应的解码器
boolean flag1 = CodecDescriptionList.isDecodeSupportedByMime(Format.VIDEO_VP9);
boolean flag2 = CodecDescriptionList.isDecodeSupportedByMime(Format.VIDEO_VP8);
HiLog.info(hiLogLabel,"video/x-vnd.on2.vp8:"+flag1+",video/x-vnd.on2.vp9:"+flag2);
判断某设备是否支持指定
MIME
对应的编码器
boolean flag3 = CodecDescriptionList.isEncodeSupportedByMime(Format.VIDEO_VP9);
boolean flag4 = CodecDescriptionList.isEncodeSupportedByMime(Format.VIDEO_VP8);
HiLog.info(hiLogLabel,"video/x-vnd.on2.vp8:"+flag3+",video/x-vnd.on2.vp9:"+flag4);
判断某设备是否支持指定
Format
的编解码器
Format format1 = new Format();
Format format2 = new Format();
format1.putStringValue(Format.MIME, Format.VIDEO_AVC);
format1.putIntValue(Format.WIDTH, 2560);
format1.putIntValue(Format.HEIGHT, 1440);
format1.putIntValue(Format.FRAME_RATE, 30);
format1.putIntValue(Format.FRAME_INTERVAL, 1);
format2.putStringValue(Format.MIME, Format.VIDEO_HEVC);
format2.putIntValue(Format.WIDTH, 1280);
format2.putIntValue(Format.HEIGHT, 720);
format2.putIntValue(Format.FRAME_RATE, 20);
format2.putIntValue(Format.FRAME_INTERVAL, 1);
boolean flag5 = CodecDescriptionList.isDecoderSupportedByFormat(format1);
boolean flag6 = CodecDescriptionList.isDecoderSupportedByFormat(format2);
HiLog.info(hiLogLabel, "format1:" + flag5 + ",format2:" + flag6);
boolean flag7 = CodecDescriptionList.isEncoderSupportedByFormat(format1);
boolean flag8 = CodecDescriptionList.isEncoderSupportedByFormat(format2);
HiLog.info(hiLogLabel, "format1:" + flag7 + ",format2:" + flag8);
2、视频编码解码
2.1、普通模式
在普通模式下进行编解码,应用必须持续地传输数据到
Codec
实例。
- 编码
/**
* @description 测试普通模式视频编码
* @author PengHuAnZhi
* @date 2021/1/15 13:24
*/
private void testCodecEncode() {
//1、创建编码Codec实例,可调用createEncoder()创建。
final Codec encoder = Codec.createEncoder();
//2、构造数据源格式,并设置给Codec实例
Format format = new Format();
format.putStringValue(Format.MIME, Format.VIDEO_AVC);
format.putIntValue(Format.WIDTH, 1920);
format.putIntValue(Format.HEIGHT, 1080);
format.putIntValue(Format.BIT_RATE, 392000);
format.putIntValue(Format.FRAME_RATE, 30);
format.putIntValue(Format.FRAME_INTERVAL, -1);
encoder.setCodecFormat(format);
//3、设置监听器
encoder.registerCodecListener(new Codec.ICodecListener() {
//读到buffer时,获取buffer的format格式,异常时抛出运行时异常,代码示例如下:
@Override
public void onReadBuffer(ByteBuffer byteBuffer, BufferInfo bufferInfo, int i) {
}
@Override
public void onError(int i, int i1, int i2) {
throw new RuntimeException();
}
});
//4、调用start方法开始编码
encoder.start();
//5、调用getAvailableBuffer()取到一个可用的ByteBuffer,把数据填入ByteBuffer里,然后再调用writeBuffer()把ByteBuffer写入编码器实例
/**
* getAvailableBuffer
* 指示编解码器可以等待ByteBuffer的持续时间(以毫秒为单位)。如果在达到指定的持续时间后没有可用的ByteBuffer,
* 则返回null值。如果将此参数设置为-1,则该方法将被阻塞,直到获得字节缓冲区或发生错误为止
*/
ByteBuffer byteBuffer = encoder.getAvailableBuffer(2000);
/**
* 。。。
* 将数据填入byteBuffer中,以下为自己写的测试数据,由于远程模拟器没有办法录视频,此章节很多demo可能无法通过测试,见谅
* 。。。
*/
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(new File(""));
byte[] bytes = new byte[2048];
fileInputStream.read(bytes);
byteBuffer.put(bytes);
} catch (IOException e) {
e.printStackTrace();
}
encoder.writeBuffer(byteBuffer, new BufferInfo());
//6、停止编码
encoder.stop();
//7、释放资源
encoder.release();
}
- 解码(和编码唯一区别就在于创建
Codec
时,调用的是createDecoder
方法)
/**
* @description 测试普通模式视频解码
* @author PengHuAnZhi
* @date 2021/1/15 13:51
*/
private void testCodecDecoder() {
//1、创建编码Codec实例,可调用createDecoder()创建。
final Codec decoder = Codec.createDecoder();
//2、构造数据源格式,并设置给Codec实例
Format format = new Format();
format.putStringValue(Format.MIME, Format.VIDEO_AVC);
format.putIntValue(Format.WIDTH, 1920);
format.putIntValue(Format.HEIGHT, 1080);
format.putIntValue(Format.BIT_RATE, 392000);
format.putIntValue(Format.FRAME_RATE, 30);
format.putIntValue(Format.FRAME_INTERVAL, -1);
decoder.setCodecFormat(format);
//3、设置监听器
decoder.registerCodecListener(new Codec.ICodecListener() {
//读到buffer时,获取buffer的format格式,异常时抛出运行时异常,代码示例如下:
@Override
public void onReadBuffer(ByteBuffer byteBuffer, BufferInfo bufferInfo, int i) {
}
@Override
public void onError(int i, int i1, int i2) {
throw new RuntimeException();
}
});
//4、调用start方法开始编码
decoder.start();
//5、调用getAvailableBuffer()取到一个可用的ByteBuffer,把数据填入ByteBuffer里,然后再调用writeBuffer()把ByteBuffer写入编码器实例
/**
* getAvailableBuffer
* 指示编解码器可以等待ByteBuffer的持续时间(以毫秒为单位)。如果在达到指定的持续时间后没有可用的ByteBuffer,
* 则返回null值。如果将此参数设置为-1,则该方法将被阻塞,直到获得字节缓冲区或发生错误为止
*/
ByteBuffer byteBuffer = decoder.getAvailableBuffer(2000);
/**
* 。。。
* 将数据填入byteBuffer中
* 。。。
*/
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(new File(""));
byte[] bytes = new byte[2048];
fileInputStream.read(bytes);
byteBuffer.put(bytes);
} catch (IOException e) {
e.printStackTrace();
}
decoder.writeBuffer(byteBuffer, new BufferInfo());
//6、停止编码
decoder.stop();
//7、释放资源
decoder.release();
}
2.2、管道模式
管道模式下应用只需要调用
Source
类的setSource()
方法,数据会自动解析并传输给Codec
实例。管道模式编码支持视频流编码和音频流编码。
- 编码
/**
* @description 测试管道模式编码
* @author PengHuAnZhi
* @date 2021/1/15 14:28
*/
private void testPipelineEncoder() {
//1、创建编码Codec实例,可调用createEncoder()创建。
final Codec encoder = Codec.createEncoder();
//2、调用setSource()设置数据源,支持设定文件路径或者文件File Descriptor。
encoder.setSource(new Source(""), new TrackInfo());
//3、构造数据源格式或者从Extractor中读取数据源格式,并设置给Codec实例,调用setSourceFormat()
Format fmt = new Format();
fmt.putStringValue(Format.MIME, Format.VIDEO_AVC);
fmt.putIntValue(Format.WIDTH, 1920);
fmt.putIntValue(Format.HEIGHT, 1080);
fmt.putIntValue(Format.BIT_RATE, 392000);
fmt.putIntValue(Format.FRAME_RATE, 30);
fmt.putIntValue(Format.FRAME_INTERVAL, -1);
encoder.setSourceFormat(fmt);
//4、设置监听器
encoder.registerCodecListener(new Codec.ICodecListener() {
//读到buffer时,获取buffer的format格式,异常时抛出运行时异常,代码示例如下:
@Override
public void onReadBuffer(ByteBuffer byteBuffer, BufferInfo bufferInfo, int i) {
}
@Override
public void onError(int i, int i1, int i2) {
throw new RuntimeException();
}
});
//5、开始编码
encoder.start();
//6、停止编码
encoder.stop();
//7、释放资源
encoder.release();
}
- 解码(和编码唯一区别就在于创建
Codec
时,调用的是createDecoder
方法)
/**
* @description 测试管道模式解码
* @author PengHuAnZhi
* @date 2021/1/15 14:28
*/
private void testPipelineDecoder() {
//1、创建编码Codec实例,可调用createDecoder()创建。
final Codec decoder = Codec.createDecoder();
//2、调用setSource()设置数据源,支持设定文件路径或者文件File Descriptor。
decoder.setSource(new Source(""), new TrackInfo());
//3、构造数据源格式或者从Extractor中读取数据源格式,并设置给Codec实例,调用setSourceFormat()
Format fmt = new Format();
fmt.putStringValue(Format.MIME, Format.VIDEO_AVC);
fmt.putIntValue(Format.WIDTH, 1920);
fmt.putIntValue(Format.HEIGHT, 1080);
fmt.putIntValue(Format.BIT_RATE, 392000);
fmt.putIntValue(Format.FRAME_RATE, 30);
fmt.putIntValue(Format.FRAME_INTERVAL, -1);
decoder.setSourceFormat(fmt);
//4、设置监听器
decoder.registerCodecListener(new Codec.ICodecListener() {
//读到buffer时,获取buffer的format格式,异常时抛出运行时异常,代码示例如下:
@Override
public void onReadBuffer(ByteBuffer byteBuffer, BufferInfo bufferInfo, int i) {
}
@Override
public void onError(int i, int i1, int i2) {
throw new RuntimeException();
}
});
//5、开始编码
decoder.start();
//6、停止编码
decoder.stop();
//7、释放资源
decoder.release();
}
3、视频播放
主要用到的
Player
类,其常用接口名如下
接口名 | 功能描述 |
---|---|
Player(Context context) | 创建Player实例。 |
setSource(Source source) | 设置媒体源。 |
prepare() | 准备播放。 |
play() | 开始播放。 |
pause() | 暂停播放。 |
stop() | 停止播放。 |
rewindTo(long microseconds) | 拖拽播放。 |
setVolume(float volume) | 调节播放音量。 |
setVideoSurface(Surface surface) | 设置视频播放的窗口。 |
enableSingleLooping(boolean looping) | 设置为单曲循环。 |
isSingleLooping() | 检查是否单曲循环播放。 |
isNowPlaying() | 检查是否播放。 |
getCurrentTime() | 获取当前播放位置。 |
getDuration() | 获取媒体文件总时长。 |
getVideoWidth() | 获取视频宽度。 |
getVideoHeight() | 获取视频高度。 |
setPlaybackSpeed(float speed) | 设置播放速度。 |
getPlaybackSpeed() | 获取播放速度。 |
setAudioStreamType(int type) | 设置音频类型。 |
getAudioStreamType() | 获取音频类型。 |
setNextPlayer(Player next) | 设置当前播放结束后的下一个播放器。 |
reset() | 重置播放器。 |
release() | 释放播放资源。 |
setPlayerCallback(IPlayerCallback callback) | 注册回调,接收播放器的事件通知或异常通知。 |
使用方法如下
/**
* @description 测试视频播放
* @author PengHuAnZhi
* @date 2021/1/15 14:41
*/
private void testVideoPlayer() {
//1、创建Player实例,可调用Player(Context context),创建本地播放器,用于在本设备播放。
final Player[] player = {new Player(this)};
//2、构造数据源对象,并调用Player实例的setSource(Source source)方法
File file = new File("/path/test_audio.mp4"); // 根据实际情况设置文件路径
FileInputStream in = null;
try {
in = new FileInputStream(file);
FileDescriptor fd = in.getFD(); // 从输入流获取FD对象
Source source = new Source(fd);
player[0].setSource(source);
} catch (IOException e) {
e.printStackTrace();
}
//3、调用prepare(),准备播放。
player[0].prepare();
//4、构造IPlayerCallback,IPlayerCallback需要实现onPlayBackComplete和onError(int errorType, int errorCode)两个方法,实现播放完成和播放异常时做相应的操作
player[0].setPlayerCallback(new Player.IPlayerCallback() {
//官方文档说的只有两个方法?????
@Override
public void onPrepared() {
}
@Override
public void onMessage(int i, int i1) {
}
//官方文档提到的两个方法之一
@Override
public void onError(int i, int i1) {
HiLog.error(hiLogLabel, "onError");
}
@Override
public void onResolutionChanged(int i, int i1) {
}
//官方文档提到的两个方法之二
@Override
public void onPlayBackComplete() {
HiLog.info(hiLogLabel, "onPlayBackComplete");
if (player[0] != null) {
player[0].stop();
player[0] = null;
}
}
@Override
public void onRewindToComplete() {
}
@Override
public void onBufferingChange(int i) {
}
@Override
public void onNewTimedMetaData(Player.MediaTimedMetaData mediaTimedMetaData) {
}
@Override
public void onMediaTimeIncontinuity(Player.MediaTimeInfo mediaTimeInfo) {
}
});
//5、开始播放
player[0].play();
//6、暂停播放
player[0].pause();
//7、拖拽功能
player[0].rewindTo(2000);//-2000
//8、获取播放总时长
int duration = player[0].getDuration();
//9、获取当前播放位置
int currentTime = player[0].getCurrentTime();
//10、结束播放
player[0].stop();
//11、释放资源
player[0].release();
}
4、视频录制
使用方法如下
/**
* @description 测试视频录制
* @author PengHuAnZhi
* @date 2021/1/15 15:21
*/
private void testRecorder() {
//1、调用Recorder()方法,创建Recorder实例。
Recorder recorder = new Recorder();
//2、构造数据源对象,并调用Recorder实例的setSource(Source source)方法,设置媒体源
Source source = new Source();
source.setRecorderAudioSource(Recorder.AudioSource.DEFAULT);
recorder.setSource(source);
//3、调用setOutputFormat(int outputFormat)方法,设置录制文件存储格式。
recorder.setOutputFormat(Recorder.OutputFormat.DEFAULT);
String path = "/path/audiotestRecord.mp4";
//4、构造存储属性StorageProperty对象,并调用Recorder实例的setStorageProperty(StorageProperty property)方法,设置录制的存储属性。
StorageProperty storageProperty = new StorageProperty.Builder()
.setRecorderPath(path)
.setRecorderMaxDurationMs(-1)
.setRecorderMaxFileSizeBytes(-1)
.build();
recorder.setStorageProperty(storageProperty);
//5、构造视频属性VideoProperty对象,并调用Recorder实例的setVideoProperty(VideoProperty property)方法,设置录制的视频属性。
VideoProperty videoProperty = new VideoProperty.Builder()
.setRecorderVideoEncoder(Recorder.VideoEncoder.DEFAULT)
.setRecorderWidth(1080)
.setRecorderDegrees(0)
.setRecorderHeight(800)
.setRecorderBitRate(10000000)
.setRecorderRate(30)
.build();
recorder.setVideoProperty(videoProperty);
//6、准备录制
recorder.prepare();
//7、开始录制
recorder.start();
//模拟录制时长
HiLog.info(hiLogLabel, "开始录制");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//8、停止录制
recorder.stop();
//9、释放资源
recorder.release();
HiLog.info(hiLogLabel, "录制结束");
File file = new File(path);
HiLog.info(hiLogLabel, file.getTotalSpace() + "");
}
5、视频提取
视频提取主要工作是将多媒体文件中的音视频数据进行分离,提取出音频、视频数据源。
使用方法如下
/**
* @description 测试视频提取
* @author PengHuAnZhi
* @date 2021/1/15 15:49
*/
private void testVideoExtract() {
//1、调用Extractor()方法创建Extractor实例。
Extractor extractor = new Extractor();
File file = new File("/path/test_audio.mp4"); // 根据实际情况设置文件路径
FileInputStream in = null;
try {
in = new FileInputStream(file);
FileDescriptor fd = in.getFD();
Source source = new Source(fd);
//2、构造数据源对象,并调用Extractor实例的setSource(Source source)方法,设置媒体源
extractor.setSource(source);
} catch (IOException e) {
e.printStackTrace();
}
//3、调用getTotalStreams()方法获取媒体的轨道数量。
int totalStreams = extractor.getTotalStreams();
//4、调用specifyStream(int id)方法选择特定轨道的数据,进行提取。
//注意官方文档这个方法名称已经变了
extractor.specifyStream(1);
//5、(可选)调用unspecifyStream(int id)方法取消选择轨道。
extractor.unspecifyStream(1);
//6、(可选)调用rewindTo(long microseconds, int mode)方法实现提取过程中的跳转指定位置。
//这第二个参数Mode我是猜测的
extractor.rewindTo(2000,Extractor.REWIND_TO_CLOSEST_SYNC);
//7、调用readBuffer(ByteBuffer buf, int offset)方法,可以实现获取提取出来的Buffer数据功能。
ByteBuffer byteBuffer = ByteBuffer.allocate(1);
extractor.readBuffer(byteBuffer,0);
//8、调用next()方法,实现提取下一帧的功能。
extractor.next();
//9、(可选)调用getStreamId()方法,可以实现获取当前选择的轨道编号的功能。
//注意官方文档这个地方方法名也变了
int streamId = extractor.getStreamId();
//10、(可选)调用getFrameTimestamp()方法,可以实现获取当前轨道内媒体数据帧时间戳的功能。
long frameTimestamp = extractor.getFrameTimestamp();
//11、(可选)调用getFrameSize()方法,可以实现获取当前轨道的媒体数据帧大小的功能。
long frameSize = extractor.getFrameSize();
//12、(可选)调用getFrameType()方法,可以实现获取当前轨道的媒体数据帧flags的功能。
int frameType = extractor.getFrameType();
//13、提取结束后,调用release()释放资源。
extractor.release();
}
6、媒体描述信息
媒体描述信息主要工作是支持多媒体的相关描述信息的存取。
使用方法如下
/**
* @description 测试获取媒体描述信息
* @author PengHuAnZhi
* @date 2021/1/15 16:10
*/
private void testAVDescription() {
//调用AVDescription.Builder类的build方法创建AVDescription实例
AVDescription avDescription = new AVDescription.Builder().setExtras(null)
.setMediaId("1")
.setDescription("Description")
.setIconUri(Uri.parse(""))
.setIMediaUri(Uri.parse(""))
.setExtras(new PacMap())
.setIcon(PixelMap.create(new PixelMap.InitializationOptions()))
.setTitle("title")
.setSubTitle("subTitle")
.build();
//(可选)根据已有的AVDescription对象,可以获取媒体的描述信息,如获取媒体Uri
Uri uri = avDescription.getMediaUri();
//(可选)根据已有的AVDescription对象,可以将媒体的描述信息写入Parcel对象
Parcel parcel = Parcel.create();
boolean result1 = avDescription.marshalling(parcel);
//(可选)根据已有的Parcel对象,可以读取到AVDescription对象,实现媒体描述信息的写入
boolean result2 = avDescription.unmarshalling(parcel);
}
7、媒体数据源开发
/**
* @description 测试媒体数据源开发
* @author PengHuAnZhi
* @date 2021/1/15 16:21
*/
private void testAVMetadata() {
//调用AVMetadata.Builder类的build方法创建AVMetadata实例。代码示例如下:
AVMetadata avMetadata = new AVMetadata.Builder().setString(AVMetadata.AVTextKey.META_ID, "illuminate.mp3")
.setString(AVMetadata.AVTextKey.TITLE, "title")
.setString(AVMetadata.AVTextKey.ARTIST, "artist")
.setString(AVMetadata.AVTextKey.ALBUM, "album")
.setString(AVMetadata.AVTextKey.SUBTITLE, "display_subtitle")
.setPixelMap(AVMetadata.AVPixelMapKey.ICON, PixelMap.create(new PixelMap.InitializationOptions()))
.build();
//(可选)根据已有的AVMetadata对象,可以获取媒体元数据信息,如获取媒体标题等
String title = avMetadata.getString(AVMetadata.AVTextKey.TITLE);
//我们需要结合AVSession使用,将已有的媒体元数据AVMetadata对象下发给应用,具体参考AVSession使用
AVSession mediaSession = new AVSession(this, AVSession.PARAM_KEY_EVENT);
mediaSession.setAVMetadata(avMetadata);
//应用获取媒体元数据一般结合AVControllerCallback相关类使用,通过onAVMetadataChanged回调获取媒体元数据。
class Callback extends AVControllerCallback {
@Override
public void onAVMetadataChanged(AVMetadata metadata) {
// 歌曲信息回调
AVDescription description = metadata.getAVDescription();
// 获取标题
String title = description.getTitle().toString();
CharSequence sequence = metadata.getText(AVMetadata.AVTextKey.TITLE);
if (sequence != null) {
title = metadata.getText(AVMetadata.AVTextKey.TITLE).toString();
}
// 设置媒体title
//musicTitle.setText(title);
// 获取曲目专封面
PixelMap iconPixelMap = description.getIcon();
// 设置歌曲封面图
//musicCover.setPixelMap(iconPixelMap);
}
}
}