赞
踩
随着自媒体的短视频和直播带货的流行,再加上这几年疫情的肆虐,直接把实体店给干趴下了。叫苦连天的实体店老板们,原来是不懂这些东西,而被割了韭菜。当然这只是开个玩笑,哈哈。他们没必要懂技术。今天我就来剖析直播的前端和后端的实现流程,干货满满,不要忘了先点个小赞,谢谢了。那么就开始我们今天要讲的内容。
Android端的推流我们需要使用到NDK,而拉流播放就简单了,使用google官方的ExoPlayer播放器进行播放即可。ExoPlayer官方中文文档[developer.android.com/media/media…] 。
在Java层,我们做一些编码推流的流程控制。
CameraHelper.kt
package site.doramusic.app.live import android.app.Activity import android.graphics.ImageFormat import android.hardware.Camera import android.hardware.Camera.CameraInfo import android.hardware.Camera.PreviewCallback import android.util.Log import android.view.Surface import android.view.SurfaceHolder class CameraHelper( private val activity: Activity, private var cameraId: Int, private var width: Int, private var height: Int ) : SurfaceHolder.Callback, PreviewCallback { private var camera: Camera? = null private var buffer: ByteArray? = null private var surfaceHolder: SurfaceHolder? = null private var previewCallback: PreviewCallback? = null private var rotation = 0 private var onChangedSizeListener: OnChangedSizeListener? = null var bytes: ByteArray? = null fun switchCamera() { cameraId = if (cameraId == CameraInfo.CAMERA_FACING_BACK) { CameraInfo.CAMERA_FACING_FRONT } else { CameraInfo.CAMERA_FACING_BACK } stopPreview() startPreview() } private fun stopPreview() { // 预览数据回调接口 camera?.setPreviewCallback(null) // 停止预览 camera?.stopPreview() // 释放摄像头 camera?.release() camera = null } private fun startPreview() { try { // 获得camera对象 camera = Camera.open(cameraId) // 配置camera的属性 val parameters = camera!!.getParameters() // 设置预览数据格式为nv21 parameters.previewFormat = ImageFormat.NV21 // 这是摄像头宽、高 setPreviewSize(parameters) // 设置摄像头 图像传感器的角度、方向 setPreviewOrientation(parameters) camera!!.setParameters(parameters) buffer = ByteArray(width * height * 3 / 2) bytes = ByteArray(buffer!!.size) // 数据缓存区 camera!!.addCallbackBuffer(buffer) camera!!.setPreviewCallbackWithBuffer(this) // 设置预览画面 camera!!.setPreviewDisplay(surfaceHolder) camera!!.startPreview() } catch (ex: Exception) { ex.printStackTrace() } } private fun setPreviewOrientation(parameters: Camera.Parameters) { val info = CameraInfo() Camera.getCameraInfo(cameraId, info) rotation = activity.windowManager.defaultDisplay.rotation var degrees = 0 when (rotation) { Surface.ROTATION_0 -> { degrees = 0 onChangedSizeListener!!.onChanged(height, width) } Surface.ROTATION_90 -> { degrees = 90 onChangedSizeListener!!.onChanged(width, height) } Surface.ROTATION_270 -> { degrees = 270 onChangedSizeListener!!.onChanged(width, height) } } var result: Int if (info.facing == CameraInfo.CAMERA_FACING_FRONT) { result = (info.orientation + degrees) % 360 result = (360 - result) % 360 // compensate the mirror } else { // back-facing result = (info.orientation - degrees + 360) % 360 } // 设置角度 camera!!.setDisplayOrientation(result) } private fun setPreviewSize(parameters: Camera.Parameters) { // 获取摄像头支持的宽、高 val supportedPreviewSizes = parameters.supportedPreviewSizes var size = supportedPreviewSizes[0] Log.d(TAG, "支持 " + size.width + "x" + size.height) // 选择一个与设置的差距最小的支持分辨率 // 10x10 20x20 30x30 // 12x12 var m = Math.abs(size.height * size.width - width * height) supportedPreviewSizes.removeAt(0) val iterator: Iterator<Camera.Size> = supportedPreviewSizes.iterator() // 遍历 while (iterator.hasNext()) { val next = iterator.next() Log.d(TAG, "支持 " + next.width + "x" + next.height) val n = Math.abs(next.height * next.width - width * height) if (n < m) { m = n size = next } } width = size.width height = size.height parameters.setPreviewSize(width, height) Log.d( TAG, "设置预览分辨率 width:" + size.width + " height:" + size.height ) } fun setPreviewDisplay(surfaceHolder: SurfaceHolder) { this.surfaceHolder = surfaceHolder this.surfaceHolder!!.addCallback(this) } fun setPreviewCallback(previewCallback: PreviewCallback) { this.previewCallback = previewCallback } override fun surfaceCreated(holder: SurfaceHolder) {} override fun surfaceChanged( holder: SurfaceHolder, format: Int, width: Int, height: Int ) { // 释放摄像头 stopPreview() // 开启摄像头 startPreview() } override fun surfaceDestroyed(holder: SurfaceHolder) { stopPreview() } override fun onPreviewFrame( data: ByteArray, camera: Camera ) { when (rotation) { Surface.ROTATION_0 -> rotation90(data) Surface.ROTATION_90 -> { } Surface.ROTATION_270 -> { } } // data数据依然是倒的 previewCallback!!.onPreviewFrame(bytes, camera) camera.addCallbackBuffer(buffer) } private fun rotation90(data: ByteArray) { var index = 0 val ySize = width * height // u和v val uvHeight = height / 2 // 后置摄像头顺时针旋转90度 if (cameraId == CameraInfo.CAMERA_FACING_BACK) { // 将y的数据旋转之后 放入新的byte数组 for (i in 0 until width) { for (j in height - 1 downTo 0) { bytes!![index++] = data[width * j + i] } } // 每次处理两个数据 var i = 0 while (i < width) { for (j in uvHeight - 1 downTo 0) { // v bytes!![index++] = data[ySize + width * j + i] // u bytes!![index++] = data[ySize + width * j + i + 1] } i += 2 } } else { // 逆时针旋转90度 for (i in 0 until width) { var nPos = width - 1 for (j in 0 until height) { bytes!![index++] = data[nPos - i] nPos += width } } // u v var i = 0 while (i < width) { var nPos = ySize + width - 1 for (j in 0 until uvHeight) { bytes!![index++] = data[nPos - i - 1] bytes!![index++] = data[nPos - i] nPos += width } i += 2 } } } fun setOnChangedSizeListener(listener: OnChangedSizeListener) { onChangedSizeListener = listener } fun release() { surfaceHolder!!.removeCallback(this) stopPreview() } interface OnChangedSizeListener { fun onChanged(w: Int, h: Int) } companion object { private const val TAG = "CameraHelper" } }
这个类实现的是使用Android系统相机预览画面的功能,包括了切换前置后置摄像头以及旋转画面。
AudioChannel.kt
package site.doramusic.app.live import android.media.AudioFormat import android.media.AudioRecord import android.media.MediaRecorder import java.util.concurrent.ExecutorService import java.util.concurrent.Executors class AudioChannel(private val livePusher: LivePusher) { private val inputSamples: Int private val executor: ExecutorService private val audioRecord: AudioRecord private val channels = 1 private var isLiving = false fun startLive() { isLiving = true executor.submit(AudioTask()) } fun stopLive() { isLiving = false } fun release() { audioRecord.release() } internal inner class AudioTask : Runnable { override fun run() { // 启动录音机 audioRecord.startRecording() val bytes = ByteArray(inputSamples) while (isLiving) { val len = audioRecord.read(bytes, 0, bytes.size) if (len > 0) { // 送去编码 livePusher.native_pushAudio(bytes) } } // 停止录音机 audioRecord.stop() } } init { executor = Executors.newSingleThreadExecutor() // 准备录音机,采集pcm数据 val channelConfig: Int channelConfig = if (channels == 2) { AudioFormat.CHANNEL_IN_STEREO } else { AudioFormat.CHANNEL_IN_MONO } livePusher.native_setAudioEncodeInfo(44100, channels) // 16位,2个字节 inputSamples = livePusher.inputSamples * 2 // 最小需要的缓冲区 val minBufferSize = AudioRecord.getMinBufferSize( 44100, channelConfig, AudioFormat.ENCODING_PCM_16BIT ) * 2 // 1、麦克风 2、采样率 3、声道数 4、采样位 audioRecord = AudioRecord( MediaRecorder.AudioSource.MIC, 44100, channelConfig, AudioFormat.ENCODING_PCM_16BIT, if (minBufferSize > inputSamples) minBufferSize else inputSamples ) } }
这个类中,我们实现了音频采样,即录音的功能。
VideoChannel.kt
package site.doramusic.app.live import android.app.Activity import android.hardware.Camera import android.hardware.Camera.PreviewCallback import android.view.SurfaceHolder import site.doramusic.app.live.CameraHelper.OnChangedSizeListener class VideoChannel( private val livePusher: LivePusher, activity: Activity, width: Int, height: Int, private val bitrate: Int, private val fps: Int, cameraId: Int ) : PreviewCallback, OnChangedSizeListener { private val cameraHelper: CameraHelper private var isLiving = false fun setPreviewDisplay(surfaceHolder: SurfaceHolder) { cameraHelper.setPreviewDisplay(surfaceHolder) } init { cameraHelper = CameraHelper(activity, cameraId, width, height) cameraHelper.setPreviewCallback(this) cameraHelper.setOnChangedSizeListener(this) } /** * 得到nv21数据,已经旋转好的。 * * @param data * @param camera */ override fun onPreviewFrame( data: ByteArray, camera: Camera ) { if (isLiving) { livePusher.native_pushVideo(data) } } fun switchCamera() { cameraHelper.switchCamera() } /** * 真实摄像头数据的宽、高。 * * @param w * @param h */ override fun onChanged(w: Int, h: Int) { // 初始化编码器 livePusher.native_setVideoEncInfo(w, h, fps, bitrate) } fun startLive() { isLiving = true } fun stopLive() { isLiving = false } fun release() { cameraHelper.release() } }
这个类中我们实现了视频编码的流程控制。
LivePusher.kt
package site.doramusic.app.live import android.app.Activity import android.view.SurfaceHolder /** * 直播推流。 */ class LivePusher( activity: Activity, width: Int, height: Int, bitrate: Int, fps: Int, cameraId: Int) { /** * 音频编码通道。 */ private val audioChannel: AudioChannel /** * 视频编码通道。 */ private val videoChannel: VideoChannel val inputSamples: Int external get companion object { init { System.loadLibrary("doralive") } } init { native_init() videoChannel = VideoChannel(this, activity, width, height, bitrate, fps, cameraId) audioChannel = AudioChannel(this) } fun setPreviewDisplay(surfaceHolder: SurfaceHolder?) { videoChannel.setPreviewDisplay(surfaceHolder) } /** * 切换前后摄像头。 */ fun switchCamera() { videoChannel.switchCamera() } fun startLive(path: String?) { native_start(path) videoChannel.startLive() audioChannel.startLive() } fun stopLive() { videoChannel.stopLive() audioChannel.stopLive() native_stop() } fun release() { videoChannel.release() audioChannel.release() native_release() } external fun native_init() external fun native_start(path: String?) external fun native_setVideoEncInfo( width: Int, height: Int, fps: Int, bitrate: Int ) external fun native_setAudioEncodeInfo(sampleRateInHz: Int, channels: Int) external fun native_pushVideo(data: ByteArray?) external fun native_stop() external fun native_release() external fun native_pushAudio(data: ByteArray?) }
最后,我们使用LivePusher这个类组合音频轨道和视频轨道数据的整合。
先看一下大致的目录结构,后文附有native层的完整代码。
AudioChannel.h
#ifndef PUSHER_AUDIO_CHANNEL_H #define PUSHER_AUDIO_CHANNEL_H #include "librtmp/rtmp.h" #include "faac/faac.h" #include <sys/types.h> class AudioChannel { typedef void (*AudioCallback)(RTMPPacket *packet); public: AudioChannel(); ~AudioChannel(); void setAudioEncInfo(int samplesInHZ, int channels); void setAudioCallback(AudioCallback audioCallback); int getInputSamples(); void encodeData(int8_t *data); RTMPPacket* getAudioTag(); private: AudioCallback m_audioCallback; int m_channels; faacEncHandle m_audioCodec = 0; u_long m_inputSamples; u_long m_maxOutputBytes; u_char *m_buffer = 0; }; #endif //PUSHER_AUDIO_CHANNEL_H
AudioChannel.cpp
#include <cstring> #include "AudioChannel.h" #include "macro.h" AudioChannel::AudioChannel() { } AudioChannel::~AudioChannel() { DELETE(m_buffer); // 释放编码器 if (m_audioCodec) { faacEncClose(m_audioCodec); m_audioCodec = 0; } } void AudioChannel::setAudioCallback(AudioCallback audioCallback) { this->m_audioCallback = audioCallback; } void AudioChannel::setAudioEncInfo(int samplesInHZ, int channels) { // 打开编码器 m_channels = channels; // 参数3:一次最大能输入编码器的样本数量,也编码的数据的个数 (一个样本是16位 2字节) // 参数4:最大可能的输出数据,编码后的最大字节数 m_audioCodec = faacEncOpen(samplesInHZ, channels, &m_inputSamples, &m_maxOutputBytes); // 设置编码器参数 faacEncConfigurationPtr config = faacEncGetCurrentConfiguration(m_audioCodec); // 指定为 mpeg4 标准 config->mpegVersion = MPEG4; // lc 标准 config->aacObjectType = LOW; // 16位 config->inputFormat = FAAC_INPUT_16BIT; // 编码出原始数据,既不是adts也不是adif config->outputFormat = 0; faacEncSetConfiguration(m_audioCodec, config); // 输出缓冲区,编码后的数据用这个缓冲区来保存 m_buffer = new u_char[m_maxOutputBytes]; } int AudioChannel::getInputSamples() { return m_inputSamples; } RTMPPacket *AudioChannel::getAudioTag() { u_char *buf; u_long len; faacEncGetDecoderSpecificInfo(m_audioCodec, &buf, &len); int bodySize = 2 + len; RTMPPacket *packet = new RTMPPacket; RTMPPacket_Alloc(packet, bodySize); // 双声道 packet->m_body[0] = 0xAF; if (m_channels == 1) { packet->m_body[0] = 0xAE; } packet->m_body[1] = 0x00; // 图片数据 memcpy(&packet->m_body[2], buf, len); packet->m_hasAbsTimestamp = 0; packet->m_nBodySize = bodySize; packet->m_packetType = RTMP_PACKET_TYPE_AUDIO; packet->m_nChannel = 0x11; packet->m_headerType = RTMP_PACKET_SIZE_LARGE; return packet; } void AudioChannel::encodeData(int8_t *data) { // 返回编码后数据字节的长度 int bytelen = faacEncEncode(m_audioCodec, reinterpret_cast<int32_t *>(data), m_inputSamples, m_buffer, m_maxOutputBytes); if (bytelen > 0) { int bodySize = 2 + bytelen; RTMPPacket *packet = new RTMPPacket; RTMPPacket_Alloc(packet, bodySize); // 双声道 packet->m_body[0] = 0xAF; if (m_channels == 1) { packet->m_body[0] = 0xAE; } // 编码出的声音,都是0x01 packet->m_body[1] = 0x01; // 图片数据 memcpy(&packet->m_body[2], m_buffer, bytelen); packet->m_hasAbsTimestamp = 0; packet->m_nBodySize = bodySize; packet->m_packetType = RTMP_PACKET_TYPE_AUDIO; packet->m_nChannel = 0x11; packet->m_headerType = RTMP_PACKET_SIZE_LARGE; m_audioCallback(packet); } }
以上为音频编码的实现。无论是音频编码还是视频编码,最终都会使用到RTMPPacket这个结构体保存数据。音频编码我们采用的是faac的库。
VideoChannel.h
#ifndef PUSHER_VIDEO_CHANNEL_H #define PUSHER_VIDEO_CHANNEL_H #include <inttypes.h> #include "x264/x264.h" #include <pthread.h> #include "librtmp/rtmp.h" class VideoChannel { typedef void (*VideoCallback)(RTMPPacket* packet); public: VideoChannel(); ~VideoChannel(); // 创建x264编码器 void setVideoEncInfo(int width, int height, int fps, int bitrate); void encodeData(int8_t *data); void setVideoCallback(VideoCallback videoCallback); private: pthread_mutex_t m_mutex; int m_width; int m_height; int m_fps; int m_bitrate; x264_t *m_videoCodec = 0; x264_picture_t *m_picIn = 0; int m_ySize; int m_uvSize; VideoCallback m_videoCallback; void sendSpsPps(uint8_t *sps, uint8_t *pps, int sps_len, int pps_len); void sendFrame(int type, uint8_t *payload, int i_payload); }; #endif //PUSHER_VIDEO_CHANNEL_H
VideoChannel.cpp
#include "VideoChannel.h" #include "librtmp/rtmp.h" #include "macro.h" #include "string.h" VideoChannel::VideoChannel() { pthread_mutex_init(&m_mutex, 0); } VideoChannel::~VideoChannel() { pthread_mutex_destroy(&m_mutex); if (m_videoCodec) { x264_encoder_close(m_videoCodec); m_videoCodec = 0; } if (m_picIn) { x264_picture_clean(m_picIn); DELETE(m_picIn); } } void VideoChannel::setVideoEncInfo(int width, int height, int fps, int bitrate) { pthread_mutex_lock(&m_mutex); m_width = width; m_height = height; m_fps = fps; m_bitrate = bitrate; m_ySize = width * height; m_uvSize = m_ySize / 4; if (m_videoCodec) { x264_encoder_close(m_videoCodec); m_videoCodec = 0; } if (m_picIn) { x264_picture_clean(m_picIn); DELETE(m_picIn); } // 打开x264编码器 // x264编码器的属性 x264_param_t param; // 参数2:最快 // 参数3:无延迟编码 x264_param_default_preset(¶m, "ultrafast", "zerolatency"); // base_line 3.2 编码规格 param.i_level_idc = 32; // 输入数据格式 param.i_csp = X264_CSP_I420; param.i_width = width; param.i_height = height; // 无b帧 param.i_bframe = 0; // 参数i_rc_method表示码率控制,CQP(恒定质量),CRF(恒定码率),ABR(平均码率) param.rc.i_rc_method = X264_RC_ABR; // 码率(比特率,单位Kbps) param.rc.i_bitrate = bitrate / 1000; // 瞬时最大码率 param.rc.i_vbv_max_bitrate = bitrate / 1000 * 1.2; // 设置了i_vbv_max_bitrate必须设置此参数,码率控制区大小,单位kbps param.rc.i_vbv_buffer_size = bitrate / 1000; // 帧率 param.i_fps_num = fps; param.i_fps_den = 1; param.i_timebase_den = param.i_fps_num; param.i_timebase_num = param.i_fps_den; // param.pf_log = x264_log_default2; // 用fps而不是时间戳来计算帧间距离 param.b_vfr_input = 0; // 帧距离(关键帧) 2s一个关键帧 param.i_keyint_max = fps * 2; // 是否复制sps和pps放在每个关键帧的前面 该参数设置是让每个关键帧(I帧)都附带sps/pps。 param.b_repeat_headers = 1; // 多线程 param.i_threads = 1; x264_param_apply_profile(¶m, "baseline"); // 打开编码器 m_videoCodec = x264_encoder_open(¶m); m_picIn = new x264_picture_t; x264_picture_alloc(m_picIn, X264_CSP_I420, width, height); pthread_mutex_unlock(&m_mutex); } void VideoChannel::setVideoCallback(VideoCallback videoCallback) { this->m_videoCallback = videoCallback; } void VideoChannel::encodeData(int8_t *data) { pthread_mutex_lock(&m_mutex); // y数据 memcpy(m_picIn->img.plane[0], data, m_ySize); for (int i = 0; i < m_uvSize; ++i) { // u数据 *(m_picIn->img.plane[1] + i) = *(data + m_ySize + i * 2 + 1); *(m_picIn->img.plane[2] + i) = *(data + m_ySize + i * 2); } // 编码出来的数据(帧数据) x264_nal_t *pp_nal; // 编码出来有几个数据(多少帧) int pi_nal; x264_picture_t pic_out; x264_encoder_encode(m_videoCodec, &pp_nal, &pi_nal, m_picIn, &pic_out); // 如果是关键帧 3 int sps_len; int pps_len; uint8_t sps[100]; uint8_t pps[100]; for (int i = 0; i < pi_nal; ++i) { if (pp_nal[i].i_type == NAL_SPS) { // 排除掉 h264的间隔 00 00 00 01 sps_len = pp_nal[i].i_payload - 4; memcpy(sps, pp_nal[i].p_payload + 4, sps_len); } else if (pp_nal[i].i_type == NAL_PPS) { pps_len = pp_nal[i].i_payload - 4; memcpy(pps, pp_nal[i].p_payload + 4, pps_len); // pps肯定是跟着sps的 sendSpsPps(sps, pps, sps_len, pps_len); } else { sendFrame(pp_nal[i].i_type, pp_nal[i].p_payload, pp_nal[i].i_payload); } } pthread_mutex_unlock(&m_mutex); } void VideoChannel::sendSpsPps(uint8_t *sps, uint8_t *pps, int sps_len, int pps_len) { int bodySize = 13 + sps_len + 3 + pps_len; RTMPPacket *packet = new RTMPPacket; RTMPPacket_Alloc(packet, bodySize); int i = 0; // 固定头 packet->m_body[i++] = 0x17; // 类型 packet->m_body[i++] = 0x00; // composition time 0x000000 packet->m_body[i++] = 0x00; packet->m_body[i++] = 0x00; packet->m_body[i++] = 0x00; // 版本 packet->m_body[i++] = 0x01; // 编码规格 packet->m_body[i++] = sps[1]; packet->m_body[i++] = sps[2]; packet->m_body[i++] = sps[3]; packet->m_body[i++] = 0xFF; // 整个sps packet->m_body[i++] = 0xE1; // sps长度 packet->m_body[i++] = (sps_len >> 8) & 0xff; packet->m_body[i++] = sps_len & 0xff; memcpy(&packet->m_body[i], sps, sps_len); i += sps_len; // pps packet->m_body[i++] = 0x01; packet->m_body[i++] = (pps_len >> 8) & 0xff; packet->m_body[i++] = (pps_len) & 0xff; memcpy(&packet->m_body[i], pps, pps_len); // 视频 packet->m_packetType = RTMP_PACKET_TYPE_VIDEO; packet->m_nBodySize = bodySize; // 随意分配一个管道(尽量避开rtmp.c中使用的) packet->m_nChannel = 10; // sps pps没有时间戳 packet->m_nTimeStamp = 0; // 不使用绝对时间 packet->m_hasAbsTimestamp = 0; packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM; m_videoCallback(packet); } void VideoChannel::sendFrame(int type, uint8_t *payload, int i_payload) { if (payload[2] == 0x00) { i_payload -= 4; payload += 4; } else { i_payload -= 3; payload += 3; } int bodySize = 9 + i_payload; RTMPPacket *packet = new RTMPPacket; RTMPPacket_Alloc(packet, bodySize); packet->m_body[0] = 0x27; if(type == NAL_SLICE_IDR){ packet->m_body[0] = 0x17; LOGE("关键帧"); } // 类型 packet->m_body[1] = 0x01; // 时间戳 packet->m_body[2] = 0x00; packet->m_body[3] = 0x00; packet->m_body[4] = 0x00; // 数据长度 int 4个字节 packet->m_body[5] = (i_payload >> 24) & 0xff; packet->m_body[6] = (i_payload >> 16) & 0xff; packet->m_body[7] = (i_payload >> 8) & 0xff; packet->m_body[8] = (i_payload) & 0xff; // 图片数据 memcpy(&packet->m_body[9], payload, i_payload); packet->m_hasAbsTimestamp = 0; packet->m_nBodySize = bodySize; packet->m_packetType = RTMP_PACKET_TYPE_VIDEO; packet->m_nChannel = 0x10; packet->m_headerType = RTMP_PACKET_SIZE_LARGE; m_videoCallback(packet); }
以上为视频编码的实现。视频编码我们采用的是x264的库。这里牵扯到了IBP帧的概念,我来简单解释下。
“IPB帧”是视频编码中的一个概念,通常用于描述视频压缩中的帧类型。在IPB帧结构中,I代表“Intra-frame”,P代表“Predictive frame”,B代表“Bidirectional frame”。
IPB帧结构的使用可以显著减小视频数据量,从而实现更高效的视频压缩和传输。
这里还有另外一个概念就是SPS和PPS。
SPS (Sequence Parameter Set) 和 PPS (Picture Parameter Set) 是 H.264/AVC(高级视频编码)标准中的两个重要概念,用于描述视频序列的参数和图像的参数。
SPS (Sequence Parameter Set) :
PPS (Picture Parameter Set) :
这两个参数集合在 H.264/AVC 中的引入,使得视频编码器可以更加灵活地进行视频压缩和解码,并且可以提高编解码器的性能和效率。SPS 和 PPS 的发送通常会随着视频流的传输一起发送,以确保解码器能够正确地解析和解码视频数据。
这些都属于音视频编解码的范畴了。
doralive.cpp
#include <jni.h> #include <string> #include "librtmp/rtmp.h" #include "safe_queue.h" #include "macro.h" #include "VideoChannel.h" #include "AudioChannel.h" SafeQueue<RTMPPacket *> packets; VideoChannel *videoChannel = 0; int isStart = 0; pthread_t pid; int readyPushing = 0; uint32_t start_time; AudioChannel *audioChannel = 0; void releasePackets(RTMPPacket *&packet) { if (packet) { RTMPPacket_Free(packet); delete packet; packet = 0; } } void callback(RTMPPacket *packet) { if (packet) { // 设置时间戳 packet->m_nTimeStamp = RTMP_GetTime() - start_time; packets.push(packet); } } extern "C" JNIEXPORT void JNICALL Java_site_doramusic_app_live_LivePusher_native_1init(JNIEnv *env, jobject instance) { // 准备一个Video编码器的工具类,进行编码 videoChannel = new VideoChannel; videoChannel->setVideoCallback(callback); audioChannel = new AudioChannel; audioChannel->setAudioCallback(callback); // 准备一个队列,打包好的数据放入队列,在线程中统一的取出数据再发送给服务器 packets.setReleaseCallback(releasePackets); } extern "C" JNIEXPORT void JNICALL Java_site_doramusic_app_live_LivePusher_native_1setVideoEncInfo(JNIEnv *env, jobject instance, jint width, jint height, jint fps, jint bitrate) { if (videoChannel) { videoChannel->setVideoEncInfo(width, height, fps, bitrate); } } void *start(void *args) { char *url = static_cast<char *>(args); RTMP *rtmp = 0; do { rtmp = RTMP_Alloc(); if (!rtmp) { LOGE("alloc rtmp失败"); break; } RTMP_Init(rtmp); int ret = RTMP_SetupURL(rtmp, url); if (!ret) { LOGE("设置地址失败:%s", url); break; } // 5s超时时间 rtmp->Link.timeout = 5; RTMP_EnableWrite(rtmp); ret = RTMP_Connect(rtmp, 0); if (!ret) { LOGE("连接服务器:%s", url); break; } ret = RTMP_ConnectStream(rtmp, 0); if (!ret) { LOGE("连接流:%s", url); break; } // 记录一个开始时间 start_time = RTMP_GetTime(); // 表示可以开始推流了 readyPushing = 1; packets.setWork(1); // 保证第一个数据是aac解码数据包 callback(audioChannel->getAudioTag()); RTMPPacket *packet = 0; while (readyPushing) { packets.pop(packet); if (!readyPushing) { break; } if (!packet) { continue; } packet->m_nInfoField2 = rtmp->m_stream_id; // 发送rtmp包 1:队列 // 意外断网?发送失败,rtmpdump 内部会调用RTMP_Close // RTMP_Close 又会调用 RTMP_SendPacket // RTMP_SendPacket 又会调用 RTMP_Close // 将rtmp.c 里面WriteN方法的 Rtmp_Close注释掉 ret = RTMP_SendPacket(rtmp, packet, 1); releasePackets(packet); if (!ret) { LOGE("发送失败"); break; } } releasePackets(packet); } while (0); isStart = 0; readyPushing = 0; packets.setWork(0); packets.clear(); if (rtmp) { RTMP_Close(rtmp); RTMP_Free(rtmp); } delete (url); return 0; } extern "C" JNIEXPORT void JNICALL Java_site_doramusic_app_live_LivePusher_native_1start(JNIEnv *env, jobject instance, jstring path_) { if (isStart) { return; } isStart = 1; const char *path = env->GetStringUTFChars(path_, 0); char *url = new char[strlen(path) + 1]; strcpy(url, path); pthread_create(&pid, 0, start, url); env->ReleaseStringUTFChars(path_, path); } extern "C" JNIEXPORT void JNICALL Java_site_doramusic_app_live_LivePusher_native_1pushVideo(JNIEnv *env, jobject instance, jbyteArray data_) { if (!videoChannel || !readyPushing) { return; } jbyte *data = env->GetByteArrayElements(data_, NULL); videoChannel->encodeData(data); env->ReleaseByteArrayElements(data_, data, 0); } extern "C" JNIEXPORT void JNICALL Java_site_doramusic_app_live_LivePusher_native_1stop(JNIEnv *env, jobject instance) { readyPushing = 0; // 关闭队列工作 packets.setWork(0); pthread_join(pid, 0); } extern "C" JNIEXPORT void JNICALL Java_site_doramusic_app_live_LivePusher_native_1release(JNIEnv *env, jobject instance) { DELETE(videoChannel); DELETE(audioChannel); } extern "C" JNIEXPORT void JNICALL Java_site_doramusic_app_live_LivePusher_native_1setAudioEncodeInfo(JNIEnv *env, jobject instance, jint sampleRateInHz, jint channels) { if (audioChannel) { audioChannel->setAudioEncInfo(sampleRateInHz, channels); } } extern "C" JNIEXPORT jint JNICALL Java_site_doramusic_app_live_LivePusher_getInputSamples(JNIEnv *env, jobject instance) { if (audioChannel) { return audioChannel->getInputSamples(); } return -1; } extern "C" JNIEXPORT void JNICALL Java_site_doramusic_app_live_LivePusher_native_1pushAudio(JNIEnv *env, jobject instance, jbyteArray data_) { if (!audioChannel || !readyPushing) { return; } jbyte *data = env->GetByteArrayElements(data_, NULL); audioChannel->encodeData(data); env->ReleaseByteArrayElements(data_, data, 0); }
这里的_1
是对Java方法名称中_
的转义。最后使用可移植操作系统的POSIX线程pthread发送rtmp包推流到服务器就完成了。
CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1) # 引入指定目录下的CMakeLists.txt add_subdirectory(src/main/cpp/librtmp) add_library( doralive SHARED src/main/cpp/doralive.cpp src/main/cpp/VideoChannel.cpp src/main/cpp/AudioChannel.cpp) include_directories(src/main/cpp/faac src/main/cpp/x264) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/cpp/libs/${ANDROID_ABI}") target_link_libraries( doralive rtmp x264 faac log)
一套完整的直播推拉流的解决方案到此就结束了。云里雾里的同学,可以系统学习下Linux操作系统的使用。对于native层代码看得头晕的同学,建议先学习cpp、jni以及音视频编解码的相关内容。
如果你看到了这里,觉得文章写得不错就给个赞呗?
更多Android进阶指南 可以扫码 解锁更多Android进阶资料
敲代码不易,关注一下吧。ღ( ´・ᴗ・` )
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。