原文:https://mp.weixin.qq.com/s?__biz=MzU3NTA3MDU1OQ==&mid=2247484865&idx=1&sn=174b8ca702466e83e72c7115d91b06ea&chksm=fd298df1ca5e04e7b2df9dc9f21e5cfe3e910204c905d8605f648ce6f6404432a83ae52a23a3&scene=178&cur_album_id=1638784435628064770#rd

MediaCodec 支持处理三种数据类型,分别是压缩数据(compressed data)、原始音频数据(raw audio data)、原始视频数据(raw video data),可以使用 ByteBuffer 处理这三种数据,也就是后文中提到的缓冲区, 可以使用 Surface 来提高编解码器性能,可以通过 ImageReader 访问原始视频帧,不能直接访问原始视频数据,通过 Image 进而获取到与之对应的 YUV 数据等其他信息。

压缩缓冲区:用于解码器的输入缓冲区和用于编码器的输出缓冲区会包含 MediaFormat 的 KEY_MIME 对应类型的压缩数据,对于视频类型,通常是单个压缩视频帧,对于音频数据,这通常是一个编码的音频段,通常包含几毫秒的音频,因格式类型而定。

原始视频缓冲区:

格式:在 ByteBuffer 模式下,视频缓冲区根据其 MediaFormat 的 KEY_COLOR_FORMAT 设置的值进行布局,可以从通过 MediaCodecInfo 相关方法获取设备受支持的颜色格式,视频编解码器可能支持三种颜色格式:

native raw video format:原始原始视频格式,由CodecCapabilities 的 COLOR_FormatSurface 常量标记,可以与输入或输出Surface一起使用。

flexible YUV buffers:灵活的 YUV 缓冲区,如 CodecCapabilities 的 COLOR_FormatYUV420Flexible 常量对应的颜色格式,可以通过 getInput、OutputImage 等于与输入、输出 Surface 以及 ByteBuffer 模式一起使用。

other specific formats:其他特定格式:通常仅在 ByteBuffer 模式下支持这些格式, 某些颜色格式是特定于供应商的,其他在均在 CodecCapabilities 中定义。

自 Android 5.1 开始,所有视频编解码器均支持灵活的 YUV 4:2:0 缓冲区。
视频帧大小于缓冲区帧大小关系:
编解码器的MediaFormat#KEY_WIDTH 和 MediaFormat#KEY_HEIGHT 键指定视频帧的大小,在大多数情况下,视频仅占据视频帧的一部分,具体表示如下:

需要使用以下键从输出格式获取原始输出图像的裁剪矩形,如果输出格式中不存在这些键,则视频将占据整个视频帧,在使用任何 MediaFormat#KEY_ROTATION 之前,也就是在设置旋转之前,可以使用下面的方式计算视频帧的大小,参考如下:

MediaFormat format = decoder.getOutputFormat(…);
int width = format.getInteger(MediaFormat.KEY_WIDTH);
if (format.containsKey("crop-left") && format.containsKey("crop-right")) {width = format.getInteger("crop-right") + 1 - format.getInteger("crop-left");
}
int height = format.getInteger(MediaFormat.KEY_HEIGHT);
if (format.containsKey("crop-top") && format.containsKey("crop-bottom")) {height = format.getInteger("crop-bottom") + 1 - format.getInteger("crop-top");
}

MediaCodec编解码的流程

客户端首先从MediaCodec获取一个空的输入缓冲区,用于填充要编码或解码的数据,
然后通过其它借口填充数据,比如解码视频用的MediaExtractor.readSampleData(inputBuffer, 0)
再将填充数据的输入缓冲区送到 MediaCodec
MediaCodec处理数据,就是编码或者解码
MediaCodec处理完数据后将数据填充到缓冲区,然后释放输入缓冲区
最后客户端获取已经编码或解码的输出缓冲区,使用完毕后释放输出缓冲区,

其编解码的流程示意图如下:

MediaCodec生命周期

MediaCodec 有三种状态,分别是执行(Executing)、停止(Stopped)和释放(Released),其中执行和停止分别有三个子状态,执行的三个字状态分别是 Flushed、Running 和 Stream-of-Stream,停止的三个子状态分别是 Uninitialized、Configured 和 Error,MediaCodec 生命周期示意图如下:


如上图所示,三种状态的切换都是由 start、stop、reset、release 等触发,根据 MediaCodec 处理数据方式的不同,其生命周期会略有不同,如在异步模式下 start 之后立即进入 Running 子状态,如果已经处于 Flushed 子状态,则需再次调用 start 进入 Running 子状态,下面是各个子状态切换对应的关键 API 如下:
停止状态(Stopped)

停止状态(Stopped)// 创建MediaCodec进入Uninitialized子状态
public static MediaCodec createByCodecName (String name)
public static MediaCodec createEncoderByType (String type)
public static MediaCodec createDecoderByType (String type)
// 配置MediaCodec进入Configured子状态,crypto和descrambler会在后文中进行说明
public void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)
public void configure(MediaFormat format, @Nullable Surface surface,int flags, MediaDescrambler descrambler)
// Error
// 编解码过程中遇到错误进入Error子状态
配置成功之后进入 执行状态(Executing) 然后start// start之后立即进入Flushed子状态
public final void start()// 客户端获取第一个输入缓冲区索引的时候进入Running子状态
public int dequeueInputBuffer (long timeoutUs)// 此时MediaCodec将不接受其他输入缓冲区,但会生成输出缓冲区
// 将填满数据的inputBuffer提交到编码队列 也是run
public void queueInputBuffer (int index, int offset, int size, long presentationTimeU)// 遇到结束符,输入缓冲区与流结束标记排队时,编解码器将转换为End-of-Stream子状态释放状态(Released)1// 编解码完成结束后释放MediaCodec进入释放状态(Released)
2public void release ()

MediaCodec的创建

前面已经提到过当创建 MediaCodec 的时候进入Uninitialized 子状态,其创建方式如下:

 1// 创建MediaCodec
2public static MediaCodec createByCodecName (String name)
3public static MediaCodec createEncoderByType (String type)
4public static MediaCodec createDecoderByType (String type)
5使用 createByCodecName 时可以借助 MediaCodecList 获取支持的编解码器,下面是获取指定 MIME 类型的编码器:1/**2 * 查询指定MIME类型的编码器3 */4fun selectCodec(mimeType: String): MediaCodecInfo? {5    val mediaCodecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)6    val codeInfos = mediaCodecList.codecInfos7    for (codeInfo in codeInfos) {8        if (!codeInfo.isEncoder) continue9        val types = codeInfo.supportedTypes
10        for (type in types) {11            if (type.equals(mimeType, true)) {12                return codeInfo
13            }
14        }
15    }
16    return null
17}当然 MediaCodecList 也提供了相应的获取编解码器的方法,如下:1// 获取指定格式的编码器
2public String findEncoderForFormat (MediaFormat format)
3// 获取指定格式的解码器
4public String findDecoderForFormat (MediaFormat format)
5

对于上述方法中的参数 MediaFormat 格式中不能包含任何帧率的设置,如果已经设置了帧率需要将其清除再使用。

上面提到了 MediaCodecList,这里简单说一下,使用 MediaCodecList 可以方便的列出当前设备支持的所有的编解码器,创建 MediaCodec 的时候要选择当前格式支持的编解码器,也就是选择的编解码器需支持对应的 MediaFormat,每个编解码器都被包装成一个 MediaCodecInfo 对象,据此可以查看该编码器的一些特性,比如是否支持硬件加速、是软解还是硬解编解码器等,常用的简单如下

 1// 是否软解2public boolean isSoftwareOnly ()3// 是Android平台提供(false)还是厂商提供(true)的编解码器4public boolean isVendor ()5// 是否支持硬件加速6public boolean isHardwareAccelerated ()7// 是编码器还是解码器8public boolean isEncoder ()9// 获取当前编解码器支持的合适
10public String[] getSupportedTypes ()
11// ...

软解和硬解应该是音视频开发中必须掌握的,当使用 MediaCodec 的时候不能说全是硬解,到底使用硬解还是软解还是要看使用的编码器,一般厂商提供的编解码器都是硬解编解码器,比如高通(qcom)等,一般如系统提供的则是软解编解码器,如带有 android 字样的编解码器,下面是本人(MI 10 Pro)自己手机的部分编解码器:

 1// 硬解编解码器2OMX.qcom.video.encoder.heic3OMX.qcom.video.decoder.avc4OMX.qcom.video.decoder.avc.secure5OMX.qcom.video.decoder.mpeg26OMX.google.gsm.decoder7OMX.qti.video.decoder.h263sw8c2.qti.avc.decoder9...
10// 软解编解码器
11c2.android.aac.decoder
12c2.android.aac.decoder
13c2.android.aac.encoder
14c2.android.aac.encoder
15c2.android.amrnb.decoder
16c2.android.amrnb.decoder
17...

MediaCodec初始化

创建 MediaCodec 之后进入 Uninitialized 子状态,此时需要对其进行一些设置如指定 MediaFormat、如果使用的是异步处理数据的方式,在 configure 之前要设置 MediaCodec.Callback,关键 API 如下:

 1// 1. MediaFormat2// 创建MediaFormat3public static final MediaFormat createVideoFormat(String mime,int width,int height)4// 开启或关闭功能,具体参见MediaCodeInfo.CodecCapabilities5public void setFeatureEnabled(@NonNull String feature, boolean enabled)6// 参数设置7public final void setInteger(String name, int value)8
// 2. setCallback
10// 如果使用的是异步处理数据的方式,在configure 之前要设置 MediaCodec.Callback
11public void setCallback (MediaCodec.Callback cb)
12public void setCallback (MediaCodec.Callback cb, Handler handler)
13
14// 3. 配置
15public void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)
16public void configure(MediaFormat format, @Nullable Surface surface,int flags, MediaDescrambler descrambler)
  • 此外某些特定格式比如 AAC 音频以及 MPEG4、H.264、H.265 视频格式,这些格式包含一些用于 MediaCodec 的初始化特定的数据,当解码处理这些压缩格式时,必须在 start 之后且在任何帧数据处理之前将这些特定数据提交给 MediaCodec,即在对 queueInputBuffer 的调用中使用标志 BUFFER_FLAG_CODEC_CONFIG 标记此类数据,这些特定的数据也可以通过 MediaFormat 设置 ByteBuffer 的方式进行配置,如下:
1// csd-0、csd-1、csd-2同理
2val bytes = byteArrayOf(0x00.toByte(), 0x01.toByte())
3mediaFormat.setByteBuffer("csd-0", ByteBuffer.wrap(bytes))

其中 csd-0、csd-1 这些键可以从 MediaExtractor#getTrackFormat 获取的MediaFormat中获取,这些特定的数据会在start 时自动提交给 MediaCodec,无需直接提交此数据,如果在输出缓冲区或格式更改之前调用了 flush,则会丢失提交的特定数据,就需要在 queueInputBuffer 的调用中使用标志 BUFFER_FLAG_CODEC_CONFIG 标记这类数据。
Android 使用以下特定于编解码器的数据缓冲区,为了正确配置 MediaMuxer 轨道,还需要将它们设置为轨道格式,每个参数集和标有(*)的编解码器专用数据部分必须以“ \ x00 \ x00 \ x00 \ x01”的起始代码开头,参考如下:

编码器在收到这些信息后将会同样输出带有BUFFER_FLAG_CODEC_CONFIG标记的 outputbuffer,此时这些数据就是特定数据,不是媒体数据。

MediaCodec数据处理方式

每个创建已经创建的编解码器都维护一组输入缓冲区,有两种处理数据的方式,同步和异步方式,根据 API 版本不同有所区别,在 API 21 也就是从 Android5.0 开始,推荐使用 ButeBuffer 的方式进行数据的处理。

MediaCodec,也就是编解码器的数据处理,主要是获取输入、输出缓冲区、提交数据给编解码器、释放输出缓冲区这几个过程,同步方式和异步方式的不同点在于输入缓冲区和输出缓冲区的其关键 API 如下:

 1// 获取输入缓冲区(同步)2public int dequeueInputBuffer (long timeoutUs)3public ByteBuffer getInputBuffer (int index)4// 获取输出缓冲区(同步)5public int dequeueOutputBuffer (MediaCodec.BufferInfo info, long timeoutUs)6public ByteBuffer getOutputBuffer (int index)7// 输入、输出缓冲区索引从MediaCodec.Callback的回调中获取,在获取对应的输入、输出缓冲区(异步)8public void setCallback (MediaCodec.Callback cb)9public void setCallback (MediaCodec.Callback cb, Handler handler)
10// 提交数据
11public void queueInputBuffer (int index, int offset, int size, long presentationTimeUs, int flags)
12public void queueSecureInputBuffer (int index, int offset, MediaCodec.CryptoInfo info, long presentationTimeUs, int flags)
13// 释放输出缓冲区
14public void releaseOutputBuffer (int index, boolean render)
15public void releaseOutputBuffer (int index, long renderTimestampNs)
16

处理数据的结束

当要处理的数据结束时(End-of-stream),需要标记流的结束,可以在最后一个有效的输入缓冲区上使用 queueInputBuffer 提交数据的时候指定 flags 为 BUFFER_FLAG_END_OF_STREAM 标记其结束,也可以在最后一个有效输入缓冲区之后提交一个空的设置了流结束标志的输入缓冲区来标记其结束,此时不能够再提交输入缓冲区,除非编解码器被 flush、stop、restart,输出缓冲区继续返回直到最终通过在 dequeueOutputBuffer 或通过 Callback#onOutputBufferAvailable 返回的 BufferInfo 中指定相同的流结束标志,最终通知输出流结束为止。
如果使用了一个输入 Surface 作为编解码器的输入,此时没有可访问的输入缓冲区,输入缓冲区会自动从这个 Surface 提交给编解码器,相当于省略了输入的这个过程,这个输入 Surface 可由 createInputSurface 方法创建,此时调用 signalEndOfInputStream 将发送流结束的信号,调用后,输入表面将立即停止向编解码器提交数据,关键 API 如下:
1// 创建输入Surface,需在configure之后、start之前调用
2public Surface createInputSurface ()
3// 设置输入Surface
4public void setInputSurface (Surface surface)
5// 发送流结束的信号
6public void signalEndOfInputStream ()
7
同理如果使用了输出 Surface,则与之相关的输出缓冲区的相关功能将会被代替,可以通过 setOutputSurface 设置一个 Surface 作为编解码器的输出,可以选择是否在输出 Surface 上渲染每一个输出缓冲区,关键 API 如下:
1// 设置输出Surface
2public void setOutputSurface (Surface surface)
3// false表示不渲染这个buffer,true表示使用默认的时间戳渲染这个buffer
4public void releaseOutputBuffer (int index, boolean render)
5// 使用指定的时间戳渲染这个buffer
6public void releaseOutputBuffer (int index, long renderTimestampNs)

自适应播放支持

当 MediaCodec 作为视频解码器的时候,可以通过如下方式检查解码器是否支持自适应播放,也就是此时解码器是否支持无缝的分辨率修改:
1// 是否支持某项功能,CodecCapabilities#FEATURE_AdaptivePlayback对应对应自适应播放支持
2public boolean isFeatureSupported (String name)
3
此时只有在将解码器配置在 Surface 上解码时,自适应播放的功能才会被激活,视频解码时当 strat 或 flush 调用后,只有关键帧(key-frame)才能完全独立解码,也就是通常说的 I 帧,其他帧都是据此来解码的,不同格式对应关键帧如下:
图片
不同的解码器对自适应播放的支持能力不同,其 seek 操作后处理也是不同,这部分内容暂时留到后续具体实践后再做整理。

seeking和自适应播放支持
无论是否支持并配置为自适应播放,视频解码器(以及一些压缩数据的编码器)在seek和格式更改的行为都不同。可以通过CodecCapatilities.isFeatureSupported(String)检查解码器是否支持自适应播放。视频解码器的自适应播放只有在将codec配置到Surface上时,才会被激活。

流边界和关键帧
重要的是,在start()或者flush()后输入数据要在合适的流边界开始:第一帧必须是关键帧。
关键帧可以通过自身被完全解码(大多数codec的I帧),并且关键帧之后没有帧要显示指的是关键帧之前的帧。
下表对不同视频格式合适的关键帧进行了总结:

不支持自适应播放的解码器(包括不解码到Surface)
为了开始解码与之前提交数据不相邻的数据,必须flush解码器。由于所有的输出buffer在flush时被立即撤销,所以需要首先发送信号,等到end-of-stream标志时再flush。重要的是,刷新后的输入数据在合适的流边界/关键帧开始。

注意,提交数据的格式在flush后必须不能更改,flush() 不支持不连续的格式。对于这种情况,一个完整的stop() - configure(…) - start() 的循环的必要的。
同时注意,如果你在start()后flush codec太频繁 - 一般来说,在首次output buffer或output format变更被接收到时 - 你需要重新提交codec-specific-data到codec。可以查看上面关于 codec-specific-data 一块的介绍。

支持并配置自适应播放的解码器
为了开始解码与之前提交数据不相邻的数据(如在seek后),不需要flush解码器。这样,输入数据是不连续的,必须从一个合适的流边界/关键帧开始。

对于某些视频格式,即H.264,H.265,VP8和VP9,也可以改变画面大小或配置中间流。为此,须将整个新的codec-specific configuration data与关键帧一起打包到单个buffer(包括任何起始代码)中,并将其作为常规输入buffer提交。
在图像大小更改发生之后及在返回新尺寸的任何帧之前,可以从dequeueOutputBuffer或onOutputFormatChanged回调中获得INFO_OUTPUT_FORMAT_CHANGED返回值。

就像codec-specific configuration data一样,在更改图片大小后不久,调用flush()时要小心,如果没有收到图片尺寸更改的确认,需要重新请求图片大小。

MediaCodec的异常处理

关于 MediaCodec 使用过程中的异常处理,这里提一下 CodecException 异常,一般是由编解码器内部异常导致的,比如媒体内容损坏、硬件故障、资源耗尽等,可以通过如下方法判断以做进一步的处理:
1// true表示可以通过stop、configure、start来恢复
2public boolean isRecoverable ()
3// true表示暂时性问题,编码或解码操作会在后续重试进行
4public boolean isTransient ()
如果 isRecoverable 和 isTransient 都是返回 false,则需要通过 reset 或 release 操作释放资源后重新工作,两者不可能同时返回 true。关于 MediaCodec 的介绍到此为止。

ImageReader

Imagee reader 类允许应用程序直接访问渲染到 Surface 中的图像数据。
多个 Android 媒体 API 类接受 Surface 对象作为渲染的目标,包括 MediaPlayer、 MediaCodec、 CameraDevice、 ImageWriter 和 RenderScript 分配。每个源使用的图像大小和格式各不相同,应该在针对特定 API 的文档中进行检查。比如 mediaCodec.configure(videoFormat, imageReader.getSurface(), null, 0);
将imageReader的surface表面传递给解码器,作为解码数据输出位置。

官方文档写的:有效的大小和格式取决于图像数据的来源。意思应该是通过ImageReader设置的大小和格式无效??

图像数据封装在 Image 对象中,可以同时访问多个这样的对象,最多可以访问 maxImages 个对象,maxImages构造函数参数指定,应该尽量小,减少内存消耗。

通过 Surface 发送到 ImageReader 的新映像将排队,直到通过 acquireLatestImage ()或 acquirenextage ()调用访问为止。由于内存限制,如果 image reader 无法获取并释放 Images,则图像源最终会在尝试将 Images 渲染到 Surface 时停止或丢弃 Images

从ImageReader的队列中获取下一幅图像。如果没有可用的新图像,则返回null。

构造方法

newInstance
为所需大小、格式和消费者使用标志的图像创建新的读取器。
maxImages参数确定可以同时从ImageReader获取的图像对象的最大数量。请求更多缓冲区会占用更多内存,因此只使用用例所需的最小数量是很重要的。
有效的大小和格式取决于图像数据的来源。
**format and usage flag组合描述消费者端点将如何使用缓冲区。**例如,如果应用程序打算将图像发送到MediaCodec或MediaRecorder进行硬件视频编码,则格式和使用标志组合需要是私有和硬件缓冲#使用#视频#编码。当ImageReader对象以有效大小和此类格式/使用标志组合创建时,应用程序可以将图像发送到ImageWriter,该ImageWriter使用MediaCodec或MediaRecorder提供的输入面创建。
如果格式是私有的,则创建的ImageReader将生成应用程序无法直接访问的图像。应用程序仍然可以从该ImageReader获取图像,并将其发送到相机进行重新处理,或通过ImageWriter接口发送到MediaCodec/MediaRecorder进行硬件视频编码。但是,getPlanes()将为私有格式的图像返回一个空数组。应用程序可以通过调用getImageFormat()检查现有读取器的格式。
与使用其他格式(如YUV_420_888)的图像阅读器相比,当应用程序不需要访问图像数据时,使用专用格式的图像阅读器更高效。
请注意,并非ImageReader支持所有格式和使用标志组合。下面是ImageReader支持的组合(假设消费者端点支持这种图像消费,例如硬件视频编码)。

acquireNextImage()
建议使用acquireNextImage()进行批处理/后台处理。不正确地使用此功能可能会导致图像显示的延迟越来越大,然后出现完全停滞,似乎没有新图像出现。

如果已使用acquireNextImage()或acquireLatestImage()获取了最大值,则此操作将因引发非法状态异常而失败。特别是,如果acquireNextImage()或acquireLatestImage()调用的序列大于maxImages,而不调用图像#介于两者之间,则会耗尽底层队列。此时,将抛出IllegalStateException,直到使用Image#close发布更多图像。

getSurface
公共表面getSurface()
获取一个可用于为该ImageReader生成图像的表面。
在将有效的图像数据渲染到此曲面之前,acquireNextImage()方法将返回null。只有一个源可以同时向该曲面生成数据,尽管一旦第一个源与曲面断开连接,同一个曲面可以通过不同的API重用。
请注意,保持此方法返回的曲面对象不足以防止其父ImageReader被回收。从这个意义上说,曲面就像是对提供它的ImageReader的弱引用。

Android音视频-MediaCodec相关推荐

  1. Android 音视频编解码(一) -- MediaCodec 初探

    音视频 系列文章 Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音):AudioTrack播放音频 Android 音视频开发(二) – Camera1 实现预览.拍 ...

  2. Android音视频开发基础(六):学习MediaCodec API,完成视频H.264的解码

    前言 在Android音视频开发中,网上知识点过于零碎,自学起来难度非常大,不过音视频大牛Jhuster提出了<Android 音视频从入门到提高 - 任务列表>.本文是Android音视 ...

  3. Android音视频之MediaCodec和MediaMuxer使用

    一.MediaCodec介绍 Android中可以使用MediaCodec来访问底层的媒体编解码器,可以对媒体进行编/解码. MediaCodec可以处理的数据有以下三种类型:压缩数据.原始音频数据. ...

  4. Android 音视频开发学习思路

    Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的.只能通过一点点的学习和积累把这块的知识串联积累起来. 初级入门篇: Android 音视频开发(一) ...

  5. Android 音视频开发入门指南

    最近收到很多网友通过邮件或者留言说想学习音视频开发,该如何入门,我今天专门写篇文章统一回复下吧. 音视频这块,目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的,希望我后面能挤出时间整 ...

  6. android硬编码封装mp4,【Android 音视频开发打怪升级:音视频硬解码篇】四、音视频解封和封装:生成一个MP4...

    [声 明] 首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正. 其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了. 最后,写文章过程 ...

  7. android音视频【十】音频mp3剪切

    人间观察 为了等你,我错过了等我的人. 介绍 Android中在一些短视频的制作app软件上,会有给视频增加背景音乐的功能,而背景音乐/歌曲(一般是mp3)是从服务器上下载后,然后本地解码,往往用户会 ...

  8. 谈谈对Android音视频开发的探究

    ​ 在日常生活中,视频类应用占据了我们越来越多的时间,各大公司也纷纷杀入这个战场,不管是抖音.快手等短视频类型,虎牙.斗鱼等直播类型,腾讯视频.爱奇艺.优酷等长视频类型,还是Vue.美拍等视频编辑美颜 ...

  9. Android 音视频难学?音视频(流媒体)开发学习也有套路

    我们都知道音视频流媒体开发这块的知识比较纷繁复杂,对新手很不友好,自学难度大,想要们既需要比较扎实的C/C++基础,又需要有很多的工程/项目经验,今天就从音视频开发/开源框架/就业方向分析,为广大开发 ...

最新文章

  1. 关于zbar的libzbar.a不支持ipnone5的64bit问题
  2. 去掉easyui datagrid内部虚线的方式。
  3. eclipse启动项目
  4. HDU1878欧拉回路
  5. IE下用iframe引入页面时出现SCRIPT5: 拒绝访问(access is denied)
  6. ffmpeg解析h264文件,提取图片信息
  7. 人类简史下载pdf_《人类简史:从动物到上帝》高清扫描版pdf下载
  8. 微信公众号支付报错:当前页面的url未注册
  9. 商品进销存管理系统、ERP系统源码
  10. mysql数字加减乘除_mysql加减乘除
  11. 人工智能驱动的智能制造(人工智能系列)
  12. 在浏览器中打开shell,连接linux
  13. 怎样才能使呼叫中心创新变得更有价值
  14. 2015年度个人总结(公司版)
  15. go语言学习:语言简介
  16. 【华为OD机试真题 Java】找出通过车辆最多颜色 (A卷2022Q4)
  17. 视频流媒体服务器ONVIF探测和云台控制是如何设置的?
  18. 数据分析[1.1]--拆解方法总结
  19. 万分之二用百分之怎么表示_万分之三怎么写?
  20. Word里表格跨页时自动断开,表格后留有空白部分,未布满整页,如何操作让表格上下页均匀布满?

热门文章

  1. 使用linux shell+sendmail+139邮箱实现定时短信报警
  2. 微博,微信,微信朋友圈,QQ分享工具类ShareUtil
  3. 苹果设备规格参数笔记
  4. 走近 WebAssembly 之调试大法
  5. 谷歌词霸 2014 发布
  6. ShineScrum10月27日高端敏捷沙龙火热报名
  7. 记一次数据库的分析和优化建议(r6笔记第24天)
  8. 科沃斯扫地机器人无语音提示_科沃斯机器人DN33常见问题汇总
  9. Flutter混合开发、安卓,ios
  10. #pragma once和#ifndef指令总结