SurfaceView和视频

SurfaceView

一篇文章看明白 Android 图形系统 Surface 与 SurfaceFlinger 之间的关系_android sufaceflinger 怎么和surface 交互的-CSDN博客

SurfaceView 和View的区别

view必须在UI的主线程中更新画面。如果更新时间过长,就会被你正在画的函数阻塞,那么将无法响应按键、触屏等消息。当使用SurfaceView由于是在新的线程中更新画面所以不会阻塞你的UI主线程。

  1. View适用于主动更新的情况,而SurfaceView则适用于被动更新的情况,比如频繁刷新界面。
  2. View在主线程中对页面进行刷新,而SurfaceView则开启一个子线程来对页面进行刷新。
  3. View在绘图时没有实现双缓冲机制,SurfaceView在底层机制中就实现了双缓冲机制

SurfaceView、TextureView

  • SurfaceView的原理就是在现有View的位置上创建一个新的Window,内容的显示和渲染都在新的Window中。这使得SurfaceView的绘制和刷新可以在单独的线程中进行,从而大大提高效率。但是呢,由于SurfaceView的内容没有显示在View中而是显示在新建的Window中, 使得SurfaceView的显示不受View的属性控制,不能进行平移,缩放等变换,也不能放在其它RecyclerView或ScrollView中,一些View中的特性也无法使用。
  • TextureView是在4.0(API level 14)引入的,专门用来渲染像视频或OpenGL场景之类的数据。与SurfaceView相比,它不会创建新的窗口来显示内容。它是将内容流直接投放到View中,并且可以和其它普通View一样进行移动,旋转,缩放,动画等变化。TextureView必须在硬件加速的窗口中使用。好在现在的移动设备基本都有GPU进行硬件加速渲染(连我手里这款破旧的华为测试机都有(o))。

SurfaceView、SurfaceHolder与Surface

  • SurfaceView 的使用方法,大概是获取 SurfaceHolder 对象,监听 surface 创建,更新,销毁,创新一个新的线程,并在其中绘制并提交
  • TextureView 并没有独立的绘图表面,在使用过程中,需要添加监听 surfaceTexture 是否可用,再做相应的处理

SurfaceView、SurfaceHolder、Surface的关系可以概括为以下几点:

  • SurfaceView是拥有独立绘图层的特殊View
  • Surface是内存中的一段绘图缓冲区。
  • SurfaceView中具有两个Surface,也就是我们所说的双缓冲机制
  • SurfaceHolder顾名思义就是Surface的持有者,SurfaceView就是通过过SurfaceHolder来对Surface进行管理控制的。并且SurfaceView.getHolder方法可以获取SurfaceView相应的SurfaceHolder。
  • Surface是在SurfaceView所在的Window可见的时候创建的。可以使用SurfaceHolder.addCallback方法来监听Surface的创建与销毁的事件。

SurfaceView的使用

  • 需要实现 SurfaceHolder.Callback 接口
  • 需要在 SurfaceHolder.Callback 的 surfaceCreated 方法中开启一个线程进行动画的逐帧的绘制。
  • 需要在 SufaceHolder.Callback 的 surfaceDestroyed 方法中结束绘画的线程并调用 SurfaceHolder 的 removeCallbck 方法
  • 绘画线程每一帧开始之前需要通过lockCanvas()方法获得Canvas对象
  • 绘制完一帧的数据之后需要使用unlockCanvasAndPost()方法将画布内容进行提交

TextureView 的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
myTexture = new TextureView(this);
myTexture.setSurfaceTextureListener(this);
setContentView(myTexture);

//实现接口
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
Logger.d("player/texture create w=%d, h=%d", width, height);
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
Logger.d("player/texture destroyed!");
if (mSurface != null) {
mSurface.release();
mSurface = null;
}
}

播放器比较

amino:VideoPlayerView
封装:VoiceController
单例初始化、开始、停止、暂停、恢复、seek、获取时长、销毁播放器封装,写了些播放状态的回调:完成、错误、播放进度

1
2
3
4
5
6
public void resume() {
if (null != player) {
isPause = false;
player.setPlayWhenReady(true);
}
}

VideoView与MediaPlayer

VideoView使用MediaPlayer + SurfaceView,SurfaceView播放视频时,如果不进行设置,视频宽高总是等于定义的。

SurfaceView布局宽高,所以视频可能会被拉伸变形。而使用VideoView时,视频宽度等于VideoView布局宽,但是高是自适应的,自动调整宽高比到视频原始比例,所以不会有拉伸(就是按比例设置的)。

VideoView不足、MediaPlayer
原生VideoView只能播放mp4和3gp两种格式;
seekTo定位不准,只能定位到关键帧
VideoView不保存视频播放状态,需自行处理;
VideoView加载网络视频时,常见黑屏情况,因为VideoView每次都会重新加载。

hls协议

hls协议:由苹果公司提出的基于HTTP的流媒体网络传输协议,它的工作原理是把整个流分成一个个小的基于HTTP的文件来下载,每次只下载一些。

HLS协议规定:
视频的封装格式是TS。
视频的编码格式为H264,音频编码格式为MP3、AAC或者AC-3。
除了TS视频文件本身,还定义了用来控制播放的m3u8文件(文本文件)
m3u8里有很多ts地址,其实就是分段来加载
ExoPlayer、ijkplayer(FFmpeg)

ExoPlayer

支持动态的自适应流 HTTP (DASH) 和 平滑流,任何目前 MediaPlayer 支持的视频格式(同时它还支持 HTTP 直播(HLS),MP4,MP3,WebM,M4A,MPEG-TS 和 AAC)。
支持高级的 HLS 特性,例如正确处理 EXT-X-DISCONTINUITY 标签;
还有各种状态
支持自定义和扩展,ExoPlayer 专门为此设计;
更少的适配性问题。

ijkplayer

SurfaceView.getHolder(),然后添加callback,在surfaceCreated里去创建播放器,测量,监听处理
surfaceHolder的监听里添加mMediaPlayer.setDisplay(holder);
playerContainer是个 FrameLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    private void addSurfaceView() {
        playerContainer.removeView(mSurfaceView);
        mSurfaceView = new ResizeSurfaceView(getContext());
        SurfaceHolder surfaceHolder = mSurfaceView.getHolder();
        surfaceHolder.addCallback(new SurfaceHolder.Callback() {

            @Override
            public void surfaceCreated(SurfaceHolder holder) {
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int
height) {
                if (mMediaPlayer != null) {
                    mMediaPlayer.setDisplay(holder);
                }
            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
            }
        });

        surfaceHolder.setFormat(PixelFormat.RGBA_8888);
        LayoutParams params = new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT,
                Gravity.CENTER);
        playerContainer.addView(mSurfaceView, 0, params);
    }

addTextureView

1
mMediaPlayer.setSurface(new Surface(surfaceTexture));

全屏逻辑
先把当前的视频view移出掉,隐藏SystemBar,然后在根view通过全屏的LayoutParams再把这个视频viwe添加进去

直播

1240

音、视频的采集、 处理(美颜、滤镜)、编码(h264、AAC)、推流、拉流
直播可以通过 UDP 降低传输延迟

播放端
ijkplayer是基于FFmpeg的跨平台播放器,这个开源项目已经被多个 App 使用,其中映客、美拍和斗鱼使用了 ijkplayer(5700+️)

推送端
个人的话GitHub上有很多开源项目。商用的话可以选择各大厂商的SDK,网易直播云、七牛、阿里、腾讯、百度、新浪都有。
使用直播云的好处就是能快速上线App,功能十分齐全,可以播放器和推流端,服务器一套下来,有专业客服人员帮助集成到工程中,缺点就是流量费太贵了,具体可以了解下各大厂商的收费标准。

软解和硬解

视频解码工作从处理器那里分离出来,交给显卡去做,这就叫做“硬解码“,纯粹的“硬解码”在现阶段是不存在的,CPU依然在发挥一部分作用,只不过硬解码时GPU/VPU已经成为运算的主力。

[[腾讯云直播]]

流程

我们分别从摄像头 / 录音设备采集数据,将数据送入编码器,分别编码出视轨 / 音轨之后,再送入合成器(MediaRemuxer 或者类似 mp4v2、FFmpeg 之类的处理库),最终输出 MP4 文件。
unknown_filename.1

对于目前的视频类 App 来说,还有各种各样的滤镜和美颜效果,实际上都可以基于 OpenGL 来实现。

视频编辑

在当下视频类 App 中,你可以见到各种视频裁剪、视频编辑的功能,例如:
裁剪视频的一部分。
多个视频进行拼接。

对于视频裁剪、拼接来说,Android 直接提供了 MediaExtractor 的接口,结合 seek 以及对应读取帧数据 readSampleData 的接口,我们可以直接获取对应时间戳的帧的内容,这样读取出来的是已经编码好的数据,因此无需重新编码,直接可以输入合成器再次合成为 MP4。
unknown_filename.2
我们只需要 seek 到需要裁剪原视频的时间戳,然后一直读取 sampleData,送入 MediaMuxer 即可,这是视频裁剪最简单的实现方式。
上面是基础的视频裁剪流程,对于视频拼接,也是类似得到多段 H.264 数据之后,才一同送入合成器。
MediaPlayer 无法精准的seek

视频特效滤镜
播放的视频可能是作为视频编辑的一部分,在剪辑时需要实时预览视频特效。我们可以简单配置播放视频的 View 为一个 GLSurfaceView,有了 OpenGL 的环境,我们就可以在这上实现各种特效、滤镜的效果了。而对于视频编辑常见的快进、倒放之类的播放配置,MediaPlayer 也有直接的接口可以设置。一般的流程是下面这样的。
unknown_filename.3
我们将解码之后的渲染交给 OpenGL,然后输出到编码器的 InputSurface 上,来实现整套编码流程。

视频播放

  1. 需要播放的视频可能本身并不在本地,很多可能都是网络视频,有边下边播的需求。
  2. 播放的视频可能是作为视频编辑的一部分,在剪辑时需要实时预览视频特效。
    对于第二种场景,我们可以简单配置播放视频的 View 为一个 GLSurfaceView,有了 OpenGL 的环境,我们就可以在这上实现各种特效、滤镜的效果了。而对于视频编辑常见的快进、倒放之类的播放配置,MediaPlayer 也有直接的接口可以设置。

虽然 MediaPlayer 也能实现在线视频播放,但实际使用下来,会有两个问题:

  • 通过设置 MediaPlayer 视频 URL 方式下载下来的视频,被放到了一个私有的位置,App 不容易直接访问,这样会导致我们没法做视频预加载,而且之前已经播放完、缓冲完的视频,也不能重复利用原有缓冲内容。
  • 同视频剪辑直接使用 MediaExtractor 返回的数据问题一样,MediaPlayer 同样无法精确 seek,只能 seek 到有关键帧的地方。

对于第一个问题,我们可以通过视频 URL 代理下载的方式来解决,通过本地使用 Local HTTP Server 的方式代理下载到一个指定的地方。现在开源社区已经有很成熟的项目实现,例如AndroidVideoCache。
而对于第二个问题来说,可以直接使用 Google 开源的ExoPlayer,简单又快捷,而且也能支持设置在线视频 URL。

边下边播

常见的网络边下边播视频的格式都是 MP4,但有些视频直接上传到服务器上的时候,我们会发现无论是使用 MediaPlayer 还是 ExoPlayer,似乎都只能等待到整个视频都下载完才能开始播放,没有达到边下边播的体验。
这个问题的原因实际上是因为 MP4 的格式导致的,具体来看,是跟 MP4格式中的 moov 有关。

MP4 格式中有一个叫作 moov 的地方存储这当前 MP4 文件的元信息,包括当前 MP4 文件的音轨视轨格式、视频长度、播放速率、视轨关键帧位置偏移量等重要信息,MP4 文件在线播放的时候,需要 moov 中的信息才能解码音轨视轨。

而上述问题发生的原因在于,当 moov 在 MP4 文件尾部的时候,播放器没有足够的信息来进行解码,因此视频变得需要直接下载完之后才能解码播放。因此,要实现 MP4 文件的边下边播,则需要将 moov 放到文件头部。目前来说,业界已经有非常成熟的工具,FFmpeg跟mp4v2都可以将一个 MP4 文件的 moov 提前放到文件头部。例如使用 FFmpeg,则是如下命令:

1
ffmpeg -i input.mp4 -movflags faststart -acodec copy -vcodec copy output.mp4

在视频播放的实践中,除了 MP4 格式来作为边下边播的格式以外,还有更多的场景需要使用其他格式,例如 m3u8、FLV 之类,业界在客户端中常见的实现包括ijkplayer、ExoPlayer,有兴趣的同学可以参考下它们的实现。

AndroidVideoCache

AndroidVideoCache是一个音视频缓存库,用于支持VideoView/MediaPlayer, ExoPlayer ,IJK等播放器的边下载边播放,按照github列出支持的特性如下:
视频文件缓存,预缓存(秒开处理),监控

1
2
3
HttpProxyCacheServer proxy = getProxy();//注意不能传入本地路径,本地的你还传进来干嘛。
String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
videoView.setVideoPath(proxyUrl);

该项目的原理其实就是将流数据源url地址转化为本地的代理服务器url
http://127.0.0.1:LocalPort/url,然后它开一个服务器一边下载缓存视频,一边把缓存的数据正常返回给你的播放器,如果已经缓存过的这里会返回一个本地文件路径

您可以使用exoPlayer与AndroidVideoCache。查看分支中的sample应用exoPlayer。注意exoPlayer也支持缓存。

视频知识

编解码

编码 就是压缩,解码 就是解压缩。视频文件的本质其实就是图片的集合而已,当一段连续的图片不断的出现在人眼前(一般一个连贯的电影或者动画至少要求一秒24帧,也就是一秒内连续出现24张图片),肉眼就会“欺骗性”的告诉大脑我们在看一个视频,而不是幻灯片。
假设一张像素为1280X720(清晰度,宽1280个像素点,高720个像素点)的图片,大小为约为1280X720X3 bytes,就是2.7MB。大家可以猜想一下为何我这里还需要乘以一个数字3.那么一段60秒钟的小电影,就需要60X24(24张图片)X2.7MB ,约为3.9GB了!
在播放器的客户端,不管是PC,手机也好,他们要显示在屏幕上的,必须是实实在在的图片啊,所以这些被压缩过的文件最终又必须被还原成图片格式,这就是解码,解压缩。

H.264/265都是编解码规范

MP4

呆坐了整整30s。那么对于这种“静态的场景”,视频压缩算法会只取这三十秒的前几帧作为基准帧图片,对其余的29s的帧,采取只保存“不同的部分”的策略,这样就不用保存这些差不多相同的图片,这种做法叫“去冗余”。大大减少了视频的体积。
mp4,rmvb,avi,他们应该被叫做容器文件(序列)。。。。因为一个容器里,不仅仅包括了视频(video)数据(轨道),还包括了(audio)音频数据,有的容器还内嵌字幕,那么就还有文字(Text)数据。
MP4 文件里面最重要的却是这个MetaData,它包含了很多关于视频的原始数据,比如视频的大小,视频的时长,还有一个索引表,这个索引表包含了不同轨道的起始位置。
举个简单的例子,有些电影包含粤语,国语两个声道。我们想换声道的时候会告诉播放器,我想听粤语,那么播放器会去索引表查找粤语的轨道起始位置,并且源源不断的读取粤语音轨的数据并播放出来。
直接看某一段视频,其实就是http的分段式下载RANGE的header

编码格式

对于我们来说,最常见的视频格式就是MP4格式,这是一个通用的容器格式。所谓容器格式,就意味内部要有对应的数据流用来承载内容。而且既然是一个视频,那必然有音轨和视轨,而音轨、视轨本身也有对应的格式。常见的音轨、视轨格式包括:

视轨:其中,目前大部分 Android 手机都支持 H.264 格式的直接硬件编码和解码;对于 H.265 来说,Android 5.0 以上的机器就支持直接硬件解码了,但是对于硬件编码,目前只有一部分高端芯片可以支持,例如高通的 8xx 系列、华为的 98x 系列。对于视轨编码来说,分辨率越大性能消耗也就越大,编码所需的时间就越长。
音轨:AAC
h265 体积差不多是同质量的 h264 的一半

码率

同一个压缩格式下,码率越高质量也就越好,码率越高越清晰,反之则画面粗糙而多马赛克
码率:影响体积,与体积成正比:码率越大,体积越大。清晰度正比
码率(码流率)=采样率 x 位深度 x 声道
文件大小 = 码率 x 时长
码率不是越大越好,码率超过一定数值,对图像的质量没有多大影响。太大体积也大,意味着更多的费用开支。
采样率(帧率)越高,就能越真实低保留【原始模拟的音视频细节】

帧率(FPS)

帧率就是在1秒钟时间里传输的图片的帧数
影响画面流畅度,与画面流畅度成正比:帧率越大,画面越流畅;帧率越小,画面越有跳动感。如果码率为变量,则帧率也会影响体积,帧率越高,每秒钟经过的画面越多,需要的码率也越高,体积也越大。
高于16的时候,就会认为是连贯的
帧率不要超过24

分辨率
影响图像大小,与图像大小成正比:分辨率越高,图像越大;分辨率越低,图像越小。


SurfaceView和视频
http://peiniwan.github.io/2024/04/ad9a539f0793.html
作者
六月的雨
发布于
2024年4月6日
许可协议