iOS (socket+H264+videoToolbox)回放视频内存增长与播放速度控制

iOS (socket+H264+videoToolbox)视频内存增长与播放速度控制
视频播放的时候 控制视频的内存在这张图的消耗速度上
在这里插入图片描述
录像是存储在某台服务器里面的,服务端一般看客户端要多少数据 ,就会开个while循环,一次读取多少字节的数据。然后几乎没有时间间隔的把数据送出去。这时候在播放端就需要做好播放速度的控制,因为接收速度完全取决于网速,如果没有做这段,那么视频的播放速度就和网速一样,网速快的时候 ,视频呈现快进的效果。网速慢的时候会出现卡帧的效果。直播的时候,由于录像采集方控制了采集的速度,所以传输给播放端的时候不需要处理播放速度。

按照标准的H264来说,应该每一帧都有pts时间戳 ,这样就可以做视频的同步来解决问题。

但就有奇葩的没有pts的H264流,该怎么处理这种流呢?

把流下载下来存文件,放到VLC里面 也能够正常播放,那么VLC是怎么做到流速控制,他怎么在没有pts的情况下知道间隔多少毫秒播放呢。用ffmpeg打印文件信息,ffprose就能什么都打印出来,拿到avformat里面的帧率 是1/25 那么就是每秒25帧,就是每隔40毫秒需要渲染一帧视频画面。

如果不是标准的25帧的帧率,那么可以通过avopeninput多等几帧来尝试解析文件,让ffmpeg

帮你猜测帧率,然后再写好等待函数的具体毫秒时间。

至于流播放的过慢应该怎么控制,可以通过一个视频缓冲区来控制,比如我们一秒钟能够保证视频

的流畅体验,那么小于25帧 就不应该播放,应该继续拉流把缓冲区至少填充到25帧,再以间隔40毫秒的速度

播放下去。而缓冲区也不能够无限的大,当视频比如说已经5秒(125帧)了。那就不要再继续拉流了。

这样可以做到流量节省,同时保证流畅的播放体验,也不会造成缓冲区过大的问题。

从上面的内容,我们知道要实现两个东西

视频缓冲区,播放间隔速度控制

在实际编码的过程中

出现了两个比较严重的问题,一个是内存增长过快,到一定程度就会被苹果杀掉了。

一个是数据拷贝的时候数据已经被其他线程释放了,导致读写的时候出现越界崩溃的问题。

还有播放速度无法控制。

针对内存增长过快,原因是拉流缓冲区释放不及时,接收速度远超过播放速度,高人提供了一个解决方案,

就是提前预分配内存,首先分配一个大概2M大小的内存,拉流收到的视频帧直接存到这个内存里面去

控制一个读写的位置,每次的新帧就会一直拼在后面,这样write的位置就会一直往后,read的位置则每读取

一包数据,就向后移动一个位置,当write写到了内存长度的最后的时候,又从头开始写,因为read已经读过了

的部分,就可以覆盖写了,这样包装所有的数据都是在这2M内的,当播放结束的时候,再把2M的内存完全释放。

这样就不用反复分配释放某个内存,而苹果是ARC的,什么时候释放也不是我们能控制的,因此这段代码就C

代码的实现,需要分配和释放成对出现,当然这从某种角度讲,也叫内存池。这里有一套java的实现,

翻译成C的就好了。

package com.temobi.util;


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class VideoBuffer {
	private static final int HEADLEN = 8;
	private static final int MAGIC = 0xabababab;
	
	private Lock lockWrite = new ReentrantLock(); 	// buffer write lock
	private int writePos; 		// write position
	private int readPos; 		// read position
	private int dataLength; 	// data length of the buffer
	private int bufferSize; 	// buffer size
	private byte[] buffer; 		// buffer

	public VideoBuffer(int size) {
		writePos = 0;
		readPos = 0;
		dataLength = 0;
		bufferSize = size;
		buffer = new byte[bufferSize];
	}

	public boolean isHasData() {
		return dataLength > HEADLEN ? true : false;
	}
	
	public int getFreeSpaceSize() {
		return bufferSize - dataLength;
	}

	//splice the buffer to tailed
	public boolean spliceBuffer(VideoBuffer buffer) {
		byte[] buf = null;
		if (!buffer.isHasData()) {
			return false;
		}

		int availableLen = makeRoom(buffer.getDataSize());
		if (availableLen < buffer.getDataSize()) {
			return false;
		}

		int len = buffer.getFirstData(buf);
		if (len > 0) {
			spliceData(buf, len);
			len = buffer.getSecondData(buf);
			if (len > 0) {
				spliceData(buf, len);
			}

			return true;
		}

		return false;
	}

	public void clearBuffer() {
		lockWrite.lock();
		try {
			writePos = 0;
			readPos = 0;
			dataLength = 0;
		} finally {
			lockWrite.unlock();
		}
	}

	public int writeOnePacket(byte[] Data, int len) {
		lockWrite.lock();
		try {
			int packetLen = len + HEADLEN;
			if (bufferSize < packetLen )
				return 0;
			
			if(getFreeSpaceSize() < packetLen) {
				int availableLen = makeRoom(packetLen);
				if (availableLen < packetLen) {
					return 0;
				}
			}
			
			byte[] head = new byte[HEADLEN];
			int2Bytes(head, 0, MAGIC, 4); 	
			int2Bytes(head, 4, packetLen, 4);
			appendData(head, HEADLEN);
			appendData(Data, len);

			return packetLen;
		} finally {
			lockWrite.unlock();
		}
	}

	// read and remove one packet frome buffer
	public int getOnePacket(byte[] outBuf) {
		lockWrite.lock();
		try {
			if (HEADLEN > dataLength) {
				return 0;
			}

			// first, read head from buffer
			byte[] buf = new byte[HEADLEN];
			int rtn = readData(buf, HEADLEN);
			if (rtn == 0)
				return 0;

			// second, parse head to get body length
			int bodyLen = bytes2Int(buf, 4) - HEADLEN;

			// third, read body from buffer
			bodyLen = readData(outBuf, bodyLen);

			return bodyLen;
		} finally {
			lockWrite.unlock();
		}
	}

	public int getFirstData(byte[] buf) {
		int len = 0;
		lockWrite.lock();
		try {
			if (writePos > readPos) {
				//**********************************
				//		^					^
				//	  readPos			   writePos
				//**********************************
				len = dataLength;
				buf = new byte[len];
				System.arraycopy(buffer, readPos, buf, 0, len);
			} else {
				//**********************************
				//		^					^
				//	  writePos			   readPos
				//**********************************
				len = bufferSize - readPos;
				if (len > 0) {
					buf = new byte[len];
					System.arraycopy(buffer, readPos, buf, 0, len);
				}
			}

			return len;
		} finally {
			lockWrite.unlock();
		}
	}

	public int getSecondData(byte[] buf) {
		int len = 0;
		lockWrite.lock();
		try {
			if (writePos < readPos) {
				len = writePos;
				buf = new byte[len];
				System.arraycopy(buffer, 0, buf, 0, len);
			}

			return len;
		} finally {
			lockWrite.unlock();
		}
	}

	public int getDataSize() {
		lockWrite.lock();
		try {
			return dataLength;
		} finally {
			lockWrite.unlock();
		}
	};

	// read and remove data from buffer
	private int readData(byte[] buf, final int dataLen) {
		lockWrite.lock();
		try {
			int firstPart;
			if (dataLen > dataLength)
				return 0;

			if ((firstPart = bufferSize - readPos) < dataLen) {
				System.arraycopy(buffer, readPos, buf, 0, firstPart);
				System.arraycopy(buffer, 0, buf, firstPart, dataLen - firstPart);
			} else {
				System.arraycopy(buffer, readPos, buf, 0, dataLen);
			}
			readPos += dataLen;

			if (readPos >= bufferSize)
				readPos -= bufferSize;

			dataLength -= dataLen;

			return dataLen;
		} finally {
			lockWrite.unlock();
		}
	}

	// read data from buffer but don't remove
	private int readDataNotRemove(byte[] buf, final int dataLen) {
		lockWrite.lock();
		try {
			if (dataLen > dataLength)
				return 0;

			int firstPart;
			if ((firstPart = bufferSize - readPos) < dataLen) {
				System.arraycopy(buffer, readPos, buf, 0, firstPart);
				System.arraycopy(buffer, 0, buf, firstPart, dataLen - firstPart);
			} else {
				System.arraycopy(buffer, readPos, buf, 0, dataLen);
			}

			return dataLen;
		} finally {
			lockWrite.unlock();
		}
	}

	// remove one packet from buffer
	private void removeOnePacket() {
		lockWrite.lock();
		try {
			if (dataLength < HEADLEN)
				return;
			byte[] buf = new byte[HEADLEN];
			int dataLen = readDataNotRemove(buf, HEADLEN);
			dataLen = bytes2Int(buf, 4);
			readPos += dataLen;
			if (readPos >= bufferSize)
				readPos -= bufferSize;

			dataLength -= dataLen;
		} finally {
			lockWrite.unlock();
		}
	}

	// append data to buffer
	private int appendData(byte[] buf, int len) {
		lockWrite.lock();
		try {
			int firstPart = 0;
			if ((firstPart = bufferSize - writePos) < len) {
				System.arraycopy(buf, 0, buffer, writePos, firstPart);
				System.arraycopy(buf, firstPart, buffer, 0, len - firstPart);
			} else {
				System.arraycopy(buf, 0, buffer, writePos, len);
			}

			writePos += len;
			if (writePos >= bufferSize)
				writePos -= bufferSize;

			dataLength += len;

			return len;
		} finally {
			lockWrite.unlock();
		}
	}

	// make room for data limit by length
	private int makeRoom(int length) {
		if(length + HEADLEN <= bufferSize) {
			while (bufferSize - dataLength < length) {
				removeOnePacket();
			}
		}
		return bufferSize - dataLength;
	}

	private void spliceData(byte[] buf, int len) {
		lockWrite.lock();
		try {
			if (writePos >= readPos) {
				int copyLen = bufferSize - writePos;
				if (copyLen < len) {
					System.arraycopy(buf, 0, buffer, writePos, copyLen);
					System.arraycopy(buf, copyLen, buffer, 0, len - copyLen);
					writePos = len - copyLen;
					dataLength += copyLen;
				} else {
					System.arraycopy(buf, 0, buffer, writePos, len);
					writePos += len;
					dataLength += copyLen;
				}
			} else {
				System.arraycopy(buf, 0, buffer, writePos, len);
				writePos += len;
				dataLength += len;
			}
		} finally {
			lockWrite.unlock();
		}
	}

	public static void java_int2C_Bytes(byte[] inByte, int nStart, int value, int len) {
		for (int i = 0; i < len; i++) {
			inByte[i + nStart] = (byte) (value >> 8 * i);
		}
	}

	public static void java_long2C_Bytes(byte[] inByte, int nStart, long value, int len) {
		for (int i = 0; i < len; i++) {
			inByte[i + nStart] = (byte) (value >> 8 * i);
		}
	}

	public static short c_bytes2Java_Short(byte[] b, int start, int len) {
		short sum = 0;
		for (int i = 0; i < len; i++) {
			short n = (short) ((short)b[i + start] & (short)0xff);
			n <<= (i) * 8;
			sum += n;
		}

		return sum;
	}
	
	public static int c_bytes2Java_Int(byte[] b, int start, int len) {
		int sum = 0;
		for (int i = 0; i < len; i++) {
			int n = b[i + start] & 0xff;
			n <<= (i) * 8;
			sum += n;
		}

		return sum;
	}

	public static long c_bytes2Java_long(byte[] b, int start, int len) {
		long sum = 0;
		for (int i = 0; i < start; i++) {
			long n = b[i] & 0xff;
			n <<= (i) * 8;
			sum += n;
		}

		return sum;
	}

	public static void int2Bytes(byte[] inByte, int nStart, int value, int len) {
		int nEnd = nStart + len;
		for (int i = 0; i < len; i++) {
			inByte[nEnd - i - 1] = (byte) (value >> 8 * i);
		}
	}

	public static void long2Bytes(byte[] inByte, int nStart, long value, int len) {
		int nEnd = nStart + len;
		for (int i = 0; i < len; i++) {
			inByte[nEnd - i - 1] = (byte) (value >> 8 * i);
		}
	}
	
	public static short bytes2Short(byte[] b, int start) {
		return (short) (b[1 + start] & 0xFF | (b[0 + start] & 0xFF) << 8);
	}

	public static int bytes2Int(byte[] b, int start) {
		return b[3 + start] & 0xFF | (b[2 + start] & 0xFF) << 8 | (b[1 + start] & 0xFF) << 16
				| (b[0 + start] & 0xFF) << 24;
	}

	public static long bytes2long(byte[] b, int start) {
		return b[7 + start] & 0xFF | (b[6 + start] & 0xFF) << 8 | (b[5 + start] & 0xFF) << 16
				| (b[4 + start] & 0xFF) << 24 | (b[3 + start] & 0xFF) << 32 | (b[2 + start] & 0xFF) << 40
				| (b[1 + start] & 0xFF) << 48 | (b[0 + start] & 0xFF) << 56;
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub

	}

}

上面的代码遇到了需要多个线程同时读写的情况, java里面有个ReentrantLock 可重入锁,

iOS中我们常用的是NSLock这种互斥锁,当已经开始写入的时候时候被加了锁,那么读取的时候再

加锁就会造成死锁了。就是不能多次加锁的意思,必须先解锁再加锁,可能有的人会说条件锁

为什么不受影响,但条件锁也是先解锁发了信号量出去再加锁的。最后找到了IOS最通用的锁

同步锁,@synchronized (self) {} 这种锁的效率最低,但是支持锁里面再加锁的。

再说内存共享的问题,为什么要内存共享,因为数据量非常大,一个高清的I帧,有的能到70K

的大小,如果到处做拷贝复制的操作,如果释放不及时,必然会造成内存的增长

socket接收端把数据发给缓冲区的时候,应该是异步拷贝放入缓冲区后立刻释放掉,

防止数据处理影响到socket下一段流的接收速度,播放端播放的时候,应该直接拿缓冲区的数据来播放,

而不是拷贝一份去播放,因为始终只有这个缓冲区的数据存在,而内存又是提前分配的,

就使得内存在一定范围内保持恒定。

应该注意缓冲区不应该过小,这样会导致write过快达到内存大小,然后又从头再写

这时候就会覆盖原来的数据,如果read速度跟不上write写的速度,那么因为数据是共享的,

当read的数据刚读取到要解码的时候,数据已经变了,这样就解析不出来帧或者有一部分数据

缺失导致花帧。因此设计的时候,应该保证缓冲区大小由外部来分配。

通过调整,在网络最快的情况下也能保证read的位置始终慢于write的位置即可。

这是一张改造完成后的内存消耗图,可以看到还是比较稳定的。

然后是播放时间的控制,按照前面的内容所讲,一开始我是开了一个while循环来通过睡眠固定的间隔36毫秒

来保障视频的渲染速度,渲染也要几毫秒,但实际在测试的过程中,发现视频最终的渲染并不是按照40毫秒的

间隔来的,精准时间可以拿CFAbsoluteTimeGetCurrent() 这个函数来看,问了一下,原来是线程的睡眠

时间并不可靠,线程间使用都是在线程在空闲的情况下,再进行下一轮睡眠,而在while循环中,可能还有其他的

内容与函数,一旦有存在等待的情况,就会导致间隔加长,视频渲染就会出现慢放的效果。

正确的写法是使用计时器,而各个平台的计时器不通用,应该用各自平台自己的写法,原因是只有各自系统

清楚有无线程问题导致计时器产生误差,苹果平台的计时器就是NSTimer和GCD两种,NSTimer的计时器误差

在50毫秒以上,而我们的计时器要求精度在36毫秒,就只能选用GCD的时间调用,GCD也是在线程里面的,

不知道是否会受到影响。

最后渲染的部分,记得使用异步串行队列实现,防止阻塞计时器,最后看下播放效果已经控制的还不错了。

目前还有两个问题,在I帧的解码需要消耗很长时间,看上去有70毫秒,不知道有没有办法解决。还有

计时器里面有可能出现没有帧需要等待的情况,这时候再等36毫秒,可能就慢了,等1毫秒,计时器本身

又变成了一个消耗性能的东西,这两点需要优化。其余已经做得很好了。

Videotoolbox在硬解码的时候,应该注意到当sps 和pps发生变化的情况下,需要重新

把init再执行一遍,不然按照旧的sps 和pps 是无法解析视频的。

拉流的内存消耗如果过快,可以考虑是否在同一线程while循环中持续拉流,可能会导致内存无法及时释放,建议拉流的业务和拉流的网络请求类分开线程处理,可以保证拉流的网络数据回调后能够尽快的释放掉

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值