鸿蒙应用开发之媒体(视频)

一、基本概念

  • 编码

编码是信息从一种形式或格式转换为另一种形式的过程。用预先规定的方法将文字、数字或其他对象编成数码,或将信息、数据转换成规定的电脉冲信号。在本模块中,编码是指编码器将原始的视频信息压缩为另一种格式的过程。

  • 解码

解码是一种用特定方法,把数码还原成它所代表的内容或将电脉冲信号、光信号、无线电波等转换成它所代表的信息、数据等的过程。在本模块中,解码是指解码器将接收到的数据还原为视频信息的过程,与编码过程相对应。

  • 帧率

帧率是以帧称为单位的位图图像连续出现在显示器上的频率(速率),以赫兹(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);
}

image-20210115113411339

判断某设备是否支持指定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);

image-20210115114344100

判断某设备是否支持指定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);

image-20210115114518208

判断某设备是否支持指定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);

image-20210115115405386

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);
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_43509535/article/details/112768064