parent
a1002eac69
commit
b096c7081e
@ -0,0 +1,103 @@ |
|||||||
|
|
||||||
|
#include <cstring> |
||||||
|
#include "AudioStream.h" |
||||||
|
#include "PushGeneric.h" |
||||||
|
|
||||||
|
AudioStream::AudioStream() { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
AudioStream::~AudioStream() { |
||||||
|
DELETE(buffer); |
||||||
|
if (audioCodec) { |
||||||
|
faacEncClose(audioCodec); |
||||||
|
audioCodec = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void AudioStream::setAudioCallback(AudioCallback audioCallback) { |
||||||
|
this->audioCallback = audioCallback; |
||||||
|
} |
||||||
|
|
||||||
|
void AudioStream::setAudioEncInfo(int samplesInHZ, int channels) { |
||||||
|
//打开编码器
|
||||||
|
mChannels = channels; |
||||||
|
//一次最大能输入编码器的样本数量 (一个样本是16位 2字节)
|
||||||
|
//编码后的最大字节数
|
||||||
|
audioCodec = faacEncOpen(static_cast<unsigned long>(samplesInHZ), |
||||||
|
static_cast<unsigned int>(channels), |
||||||
|
&inputSamples, |
||||||
|
&maxOutputBytes); |
||||||
|
|
||||||
|
//设置编码器参数
|
||||||
|
faacEncConfigurationPtr config = faacEncGetCurrentConfiguration(audioCodec); |
||||||
|
//指定为 mpeg4 标准
|
||||||
|
config->mpegVersion = MPEG4; |
||||||
|
//lc 标准
|
||||||
|
config->aacObjectType = LOW; |
||||||
|
//16位
|
||||||
|
config->inputFormat = FAAC_INPUT_16BIT; |
||||||
|
// 编码出原始数据
|
||||||
|
config->outputFormat = 0; |
||||||
|
faacEncSetConfiguration(audioCodec, config); |
||||||
|
|
||||||
|
//输出缓冲区 编码后的数据 用这个缓冲区来保存
|
||||||
|
buffer = new u_char[maxOutputBytes]; |
||||||
|
} |
||||||
|
|
||||||
|
int AudioStream::getInputSamples() { |
||||||
|
return static_cast<int>(inputSamples); |
||||||
|
} |
||||||
|
|
||||||
|
RTMPPacket *AudioStream::getAudioTag() { |
||||||
|
u_char *buf; |
||||||
|
u_long len; |
||||||
|
faacEncGetDecoderSpecificInfo(audioCodec, &buf, &len); |
||||||
|
int bodySize = static_cast<int>(2 + len); |
||||||
|
RTMPPacket *packet = new RTMPPacket; |
||||||
|
RTMPPacket_Alloc(packet, bodySize); |
||||||
|
//双声道
|
||||||
|
packet->m_body[0] = 0xAF; |
||||||
|
if (mChannels == 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 AudioStream::encodeData(int8_t *data) { |
||||||
|
//返回编码后数据字节的长度
|
||||||
|
int byteLen = faacEncEncode(audioCodec, reinterpret_cast<int32_t *>(data), |
||||||
|
static_cast<unsigned int>(inputSamples), |
||||||
|
buffer, |
||||||
|
static_cast<unsigned int>(maxOutputBytes)); |
||||||
|
if (byteLen > 0) { |
||||||
|
int bodySize = 2 + byteLen; |
||||||
|
RTMPPacket *packet = new RTMPPacket; |
||||||
|
RTMPPacket_Alloc(packet, bodySize); |
||||||
|
//双声道
|
||||||
|
packet->m_body[0] = 0xAF; |
||||||
|
if (mChannels == 1) { |
||||||
|
packet->m_body[0] = 0xAE; |
||||||
|
} |
||||||
|
|
||||||
|
packet->m_body[1] = 0x01; |
||||||
|
memcpy(&packet->m_body[2], buffer, static_cast<size_t>(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; |
||||||
|
audioCallback(packet); |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -0,0 +1,36 @@ |
|||||||
|
|
||||||
|
#ifndef AUDIOSTREAM_H |
||||||
|
#define AUDIOSTREAM_H |
||||||
|
|
||||||
|
#include "include/rtmp/rtmp.h" |
||||||
|
#include "include/faac/faac.h" |
||||||
|
#include <sys/types.h> |
||||||
|
|
||||||
|
class AudioStream { |
||||||
|
typedef void (*AudioCallback)(RTMPPacket *packet); |
||||||
|
|
||||||
|
public: |
||||||
|
AudioStream(); |
||||||
|
|
||||||
|
~AudioStream(); |
||||||
|
|
||||||
|
void setAudioEncInfo(int samplesInHZ, int channels); |
||||||
|
|
||||||
|
void setAudioCallback(AudioCallback audioCallback); |
||||||
|
|
||||||
|
int getInputSamples(); |
||||||
|
|
||||||
|
void encodeData(int8_t *data); |
||||||
|
|
||||||
|
RTMPPacket* getAudioTag(); |
||||||
|
private: |
||||||
|
AudioCallback audioCallback; |
||||||
|
int mChannels; |
||||||
|
faacEncHandle audioCodec = 0; |
||||||
|
u_long inputSamples; |
||||||
|
u_long maxOutputBytes; |
||||||
|
u_char *buffer = 0; |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
#endif |
@ -0,0 +1,12 @@ |
|||||||
|
|
||||||
|
#ifndef PUSHGENERIC_H |
||||||
|
#define PUSHGENERIC_H |
||||||
|
|
||||||
|
#include <android/log.h> |
||||||
|
|
||||||
|
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,"FrankLive",__VA_ARGS__) |
||||||
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"FrankLive",__VA_ARGS__) |
||||||
|
|
||||||
|
#define DELETE(obj) if(obj){ delete obj; obj = 0; } |
||||||
|
|
||||||
|
#endif |
@ -0,0 +1,239 @@ |
|||||||
|
#include <jni.h> |
||||||
|
#include <string> |
||||||
|
#include "include/rtmp/rtmp.h" |
||||||
|
#include "safe_queue.h" |
||||||
|
#include "PushGeneric.h" |
||||||
|
#include "VideoStream.h" |
||||||
|
#include "AudioStream.h" |
||||||
|
|
||||||
|
SafeQueue<RTMPPacket *> packets; |
||||||
|
VideoStream *videoChannel = 0; |
||||||
|
int isStart = 0; |
||||||
|
pthread_t pid; |
||||||
|
|
||||||
|
int readyPushing = 0; |
||||||
|
uint32_t start_time; |
||||||
|
|
||||||
|
AudioStream *audioChannel = 0; |
||||||
|
|
||||||
|
//子线程回调给Java需要用到JavaVM
|
||||||
|
JavaVM *javaVM; |
||||||
|
//调用类
|
||||||
|
jobject jobject_error; |
||||||
|
|
||||||
|
/***************与Java层对应**************/ |
||||||
|
//视频编码器打开失败
|
||||||
|
const int ERROR_VIDEO_ENCODER_OPEN = 0x01; |
||||||
|
//视频帧编码失败
|
||||||
|
const int ERROR_VIDEO_ENCODE = 0x02; |
||||||
|
//音频编码器打开失败
|
||||||
|
const int ERROR_AUDIO_ENCODER_OPEN = 0x03; |
||||||
|
//音频帧编码失败
|
||||||
|
const int ERROR_AUDIO_ENCODE = 0x04; |
||||||
|
//RTMP连接失败
|
||||||
|
const int ERROR_RTMP_CONNECT = 0x05; |
||||||
|
//RTMP连接流失败
|
||||||
|
const int ERROR_RTMP_CONNECT_STREAM = 0x06; |
||||||
|
//RTMP发送数据包失败
|
||||||
|
const int ERROR_RTMP_SEND_PACKET = 0x07; |
||||||
|
|
||||||
|
/***************与Java层对应**************/ |
||||||
|
|
||||||
|
//当调用System.loadLibrary时,会回调这个方法
|
||||||
|
jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { |
||||||
|
javaVM = vm; |
||||||
|
return JNI_VERSION_1_6; |
||||||
|
} |
||||||
|
|
||||||
|
//回调异常给java
|
||||||
|
void throwErrToJava(int error_code) { |
||||||
|
JNIEnv *env; |
||||||
|
javaVM->AttachCurrentThread(&env, NULL); |
||||||
|
jclass classErr = env->GetObjectClass(jobject_error); |
||||||
|
jmethodID methodErr = env->GetMethodID(classErr, "errorFromNative", "(I)V"); |
||||||
|
env->CallVoidMethod(jobject_error, methodErr, error_code); |
||||||
|
javaVM->DetachCurrentThread(); |
||||||
|
} |
||||||
|
|
||||||
|
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_com_frank_live_LivePusherNew_native_1init(JNIEnv *env, jobject instance) { |
||||||
|
videoChannel = new VideoStream; |
||||||
|
videoChannel->setVideoCallback(callback); |
||||||
|
audioChannel = new AudioStream; |
||||||
|
audioChannel->setAudioCallback(callback); |
||||||
|
packets.setReleaseCallback(releasePackets); |
||||||
|
jobject_error = env->NewGlobalRef(instance); |
||||||
|
} |
||||||
|
|
||||||
|
extern "C" |
||||||
|
JNIEXPORT void JNICALL |
||||||
|
Java_com_frank_live_LivePusherNew_native_1setVideoCodecInfo(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("RTMP_Alloc fail"); |
||||||
|
break; |
||||||
|
} |
||||||
|
RTMP_Init(rtmp); |
||||||
|
int ret = RTMP_SetupURL(rtmp, url); |
||||||
|
if (!ret) { |
||||||
|
LOGE("RTMP_SetupURL:%s", url); |
||||||
|
break; |
||||||
|
} |
||||||
|
//超时时间
|
||||||
|
rtmp->Link.timeout = 5; |
||||||
|
RTMP_EnableWrite(rtmp); |
||||||
|
ret = RTMP_Connect(rtmp, 0); |
||||||
|
if (!ret) { |
||||||
|
LOGE("RTMP_Connect:%s", url); |
||||||
|
throwErrToJava(ERROR_RTMP_CONNECT); |
||||||
|
break; |
||||||
|
} |
||||||
|
ret = RTMP_ConnectStream(rtmp, 0); |
||||||
|
if (!ret) { |
||||||
|
LOGE("RTMP_ConnectStream:%s", url); |
||||||
|
throwErrToJava(ERROR_RTMP_CONNECT_STREAM); |
||||||
|
break; |
||||||
|
} |
||||||
|
//开始时间
|
||||||
|
start_time = RTMP_GetTime(); |
||||||
|
//开始推流
|
||||||
|
readyPushing = 1; |
||||||
|
packets.setWork(1); |
||||||
|
callback(audioChannel->getAudioTag()); |
||||||
|
RTMPPacket *packet = 0; |
||||||
|
while (readyPushing) { |
||||||
|
packets.pop(packet); |
||||||
|
if (!readyPushing) { |
||||||
|
break; |
||||||
|
} |
||||||
|
if (!packet) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
packet->m_nInfoField2 = rtmp->m_stream_id; |
||||||
|
ret = RTMP_SendPacket(rtmp, packet, 1); |
||||||
|
releasePackets(packet); |
||||||
|
if (!ret) { |
||||||
|
LOGE("RTMP_SendPacket fail..."); |
||||||
|
throwErrToJava(ERROR_RTMP_SEND_PACKET); |
||||||
|
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_com_frank_live_LivePusherNew_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_com_frank_live_LivePusherNew_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_com_frank_live_LivePusherNew_native_1setAudioCodecInfo(JNIEnv *env, jobject instance, |
||||||
|
jint sampleRateInHz, |
||||||
|
jint channels) { |
||||||
|
if (audioChannel) { |
||||||
|
audioChannel->setAudioEncInfo(sampleRateInHz, channels); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
extern "C" |
||||||
|
JNIEXPORT jint JNICALL |
||||||
|
Java_com_frank_live_LivePusherNew_getInputSamples(JNIEnv *env, jobject instance) { |
||||||
|
|
||||||
|
if (audioChannel) { |
||||||
|
return audioChannel->getInputSamples(); |
||||||
|
} |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
extern "C" |
||||||
|
JNIEXPORT void JNICALL |
||||||
|
Java_com_frank_live_LivePusherNew_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); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
extern "C" |
||||||
|
JNIEXPORT void JNICALL |
||||||
|
Java_com_frank_live_LivePusherNew_native_1stop(JNIEnv *env, jobject instance) { |
||||||
|
readyPushing = 0; |
||||||
|
packets.setWork(0); |
||||||
|
pthread_join(pid, 0); |
||||||
|
} |
||||||
|
|
||||||
|
extern "C" |
||||||
|
JNIEXPORT void JNICALL |
||||||
|
Java_com_frank_live_LivePusherNew_native_1release(JNIEnv *env, jobject instance) { |
||||||
|
env->DeleteGlobalRef(jobject_error); |
||||||
|
DELETE(videoChannel); |
||||||
|
DELETE(audioChannel); |
||||||
|
} |
@ -0,0 +1,207 @@ |
|||||||
|
|
||||||
|
#include "VideoStream.h" |
||||||
|
#include "include/rtmp/rtmp.h" |
||||||
|
#include "PushGeneric.h" |
||||||
|
|
||||||
|
VideoStream::VideoStream() { |
||||||
|
pthread_mutex_init(&mutex, 0); |
||||||
|
} |
||||||
|
|
||||||
|
VideoStream::~VideoStream() { |
||||||
|
pthread_mutex_destroy(&mutex); |
||||||
|
if (videoCodec) { |
||||||
|
x264_encoder_close(videoCodec); |
||||||
|
videoCodec = 0; |
||||||
|
} |
||||||
|
if (pic_in) { |
||||||
|
x264_picture_clean(pic_in); |
||||||
|
DELETE(pic_in); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void VideoStream::setVideoEncInfo(int width, int height, int fps, int bitrate) { |
||||||
|
pthread_mutex_lock(&mutex); |
||||||
|
mWidth = width; |
||||||
|
mHeight = height; |
||||||
|
mFps = fps; |
||||||
|
mBitrate = bitrate; |
||||||
|
ySize = width * height; |
||||||
|
uvSize = ySize / 4; |
||||||
|
if (videoCodec) { |
||||||
|
x264_encoder_close(videoCodec); |
||||||
|
videoCodec = 0; |
||||||
|
} |
||||||
|
if (pic_in) { |
||||||
|
x264_picture_clean(pic_in); |
||||||
|
DELETE(pic_in); |
||||||
|
} |
||||||
|
|
||||||
|
//打开x264编码器
|
||||||
|
//x264编码器的属性
|
||||||
|
x264_param_t param; |
||||||
|
x264_param_default_preset(¶m, "ultrafast", "zerolatency"); |
||||||
|
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; |
||||||
|
//用fps而不是时间戳来计算帧间距离
|
||||||
|
param.b_vfr_input = 0; |
||||||
|
//帧距离(关键帧) 2s一个关键帧
|
||||||
|
param.i_keyint_max = fps * 2; |
||||||
|
//设置是让每个关键帧(I帧)都附带sps/pps
|
||||||
|
param.b_repeat_headers = 1; |
||||||
|
//多线程
|
||||||
|
param.i_threads = 1; |
||||||
|
|
||||||
|
x264_param_apply_profile(¶m, "baseline"); |
||||||
|
//打开编码器
|
||||||
|
videoCodec = x264_encoder_open(¶m); |
||||||
|
pic_in = new x264_picture_t; |
||||||
|
x264_picture_alloc(pic_in, X264_CSP_I420, width, height); |
||||||
|
pthread_mutex_unlock(&mutex); |
||||||
|
} |
||||||
|
|
||||||
|
void VideoStream::setVideoCallback(VideoCallback videoCallback) { |
||||||
|
this->videoCallback = videoCallback; |
||||||
|
} |
||||||
|
|
||||||
|
void VideoStream::encodeData(int8_t *data) { |
||||||
|
pthread_mutex_lock(&mutex); |
||||||
|
//y数据
|
||||||
|
memcpy(pic_in->img.plane[0], data, ySize); |
||||||
|
for (int i = 0; i < uvSize; ++i) { |
||||||
|
//u数据
|
||||||
|
*(pic_in->img.plane[1] + i) = *(data + ySize + i * 2 + 1); |
||||||
|
*(pic_in->img.plane[2] + i) = *(data + ySize + i * 2); |
||||||
|
} |
||||||
|
|
||||||
|
x264_nal_t *pp_nal; |
||||||
|
int pi_nal; |
||||||
|
x264_picture_t pic_out; |
||||||
|
x264_encoder_encode(videoCodec, &pp_nal, &pi_nal, pic_in, &pic_out); |
||||||
|
int sps_len = 0; |
||||||
|
int pps_len = 0; |
||||||
|
uint8_t sps[100]; |
||||||
|
uint8_t pps[100]; |
||||||
|
for (int i = 0; i < pi_nal; ++i) { |
||||||
|
if (pp_nal[i].i_type == NAL_SPS) { |
||||||
|
sps_len = pp_nal[i].i_payload - 4; |
||||||
|
memcpy(sps, pp_nal[i].p_payload + 4, static_cast<size_t>(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, static_cast<size_t>(pps_len)); |
||||||
|
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(&mutex); |
||||||
|
} |
||||||
|
|
||||||
|
void VideoStream::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; |
||||||
|
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; |
||||||
|
|
||||||
|
videoCallback(packet); |
||||||
|
} |
||||||
|
|
||||||
|
void VideoStream::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("IDR key frame"); |
||||||
|
} |
||||||
|
//类型
|
||||||
|
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, static_cast<size_t>(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; |
||||||
|
videoCallback(packet); |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
|
||||||
|
#ifndef VIDEOSTREAM_H |
||||||
|
#define VIDEOSTREAM_H |
||||||
|
|
||||||
|
#include <inttypes.h> |
||||||
|
#include <pthread.h> |
||||||
|
#include "include/rtmp/rtmp.h" |
||||||
|
#include "include/x264/x264.h" |
||||||
|
|
||||||
|
class VideoStream { |
||||||
|
typedef void (*VideoCallback)(RTMPPacket* packet); |
||||||
|
public: |
||||||
|
VideoStream(); |
||||||
|
|
||||||
|
~VideoStream(); |
||||||
|
|
||||||
|
//创建x264编码器
|
||||||
|
void setVideoEncInfo(int width, int height, int fps, int bitrate); |
||||||
|
|
||||||
|
void encodeData(int8_t *data); |
||||||
|
|
||||||
|
void setVideoCallback(VideoCallback videoCallback); |
||||||
|
|
||||||
|
private: |
||||||
|
pthread_mutex_t mutex; |
||||||
|
int mWidth; |
||||||
|
int mHeight; |
||||||
|
int mFps; |
||||||
|
int mBitrate; |
||||||
|
x264_t *videoCodec = 0; |
||||||
|
x264_picture_t *pic_in = 0; |
||||||
|
|
||||||
|
int ySize; |
||||||
|
int uvSize; |
||||||
|
VideoCallback 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 |
@ -0,0 +1,169 @@ |
|||||||
|
|
||||||
|
#ifndef SAFE_QUEUE_H |
||||||
|
#define SAFE_QUEUE_H |
||||||
|
|
||||||
|
#include <queue> |
||||||
|
#include <pthread.h> |
||||||
|
|
||||||
|
#ifdef C11 |
||||||
|
#include <thread> |
||||||
|
#endif |
||||||
|
|
||||||
|
using namespace std; |
||||||
|
|
||||||
|
template<typename T> |
||||||
|
class SafeQueue { |
||||||
|
typedef void (*ReleaseCallback)(T &); |
||||||
|
|
||||||
|
typedef void (*SyncHandle)(queue<T> &); |
||||||
|
|
||||||
|
public: |
||||||
|
SafeQueue() { |
||||||
|
#ifdef C11 |
||||||
|
|
||||||
|
#else |
||||||
|
pthread_mutex_init(&mutex, NULL); |
||||||
|
pthread_cond_init(&cond, NULL); |
||||||
|
#endif |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
~SafeQueue() { |
||||||
|
#ifdef C11 |
||||||
|
#else |
||||||
|
pthread_cond_destroy(&cond); |
||||||
|
pthread_mutex_destroy(&mutex); |
||||||
|
#endif |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
void push( T new_value) { |
||||||
|
#ifdef C11 |
||||||
|
//锁 和智能指针原理类似,自动释放
|
||||||
|
lock_guard<mutex> lk(mt); |
||||||
|
if (work) { |
||||||
|
q.push(new_value); |
||||||
|
cv.notify_one(); |
||||||
|
} |
||||||
|
#else |
||||||
|
pthread_mutex_lock(&mutex); |
||||||
|
if (work) { |
||||||
|
q.push(new_value); |
||||||
|
pthread_cond_signal(&cond); |
||||||
|
}else{ |
||||||
|
releaseCallback(new_value); |
||||||
|
} |
||||||
|
pthread_mutex_unlock(&mutex); |
||||||
|
#endif |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
int pop(T& value) { |
||||||
|
int ret = 0; |
||||||
|
#ifdef C11 |
||||||
|
//占用空间相对lock_guard 更大一点且相对更慢一点,但是配合条件必须使用它,更灵活
|
||||||
|
unique_lock<mutex> lk(mt); |
||||||
|
//第二个参数 lambda表达式:false则不阻塞 往下走
|
||||||
|
cv.wait(lk,[this]{return !work || !q.empty();}); |
||||||
|
if (!q.empty()) { |
||||||
|
value = q.front(); |
||||||
|
q.pop(); |
||||||
|
ret = 1; |
||||||
|
} |
||||||
|
#else |
||||||
|
pthread_mutex_lock(&mutex); |
||||||
|
|
||||||
|
while (work && q.empty()) { |
||||||
|
pthread_cond_wait(&cond, &mutex); |
||||||
|
} |
||||||
|
if (!q.empty()) { |
||||||
|
value = q.front(); |
||||||
|
q.pop(); |
||||||
|
ret = 1; |
||||||
|
} |
||||||
|
pthread_mutex_unlock(&mutex); |
||||||
|
#endif |
||||||
|
return ret; |
||||||
|
} |
||||||
|
|
||||||
|
void setWork(int work) { |
||||||
|
#ifdef C11 |
||||||
|
lock_guard<mutex> lk(mt); |
||||||
|
this->work = work; |
||||||
|
#else |
||||||
|
pthread_mutex_lock(&mutex); |
||||||
|
this->work = work; |
||||||
|
pthread_cond_signal(&cond); |
||||||
|
pthread_mutex_unlock(&mutex); |
||||||
|
#endif |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
int empty() { |
||||||
|
return q.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
int size() { |
||||||
|
return q.size(); |
||||||
|
} |
||||||
|
|
||||||
|
void clear() { |
||||||
|
#ifdef C11 |
||||||
|
lock_guard<mutex> lk(mt); |
||||||
|
int size = q.size(); |
||||||
|
for (int i = 0; i < size; ++i) { |
||||||
|
T value = q.front(); |
||||||
|
releaseHandle(value); |
||||||
|
q.pop(); |
||||||
|
} |
||||||
|
#else |
||||||
|
pthread_mutex_lock(&mutex); |
||||||
|
int size = q.size(); |
||||||
|
for (int i = 0; i < size; ++i) { |
||||||
|
T value = q.front(); |
||||||
|
releaseCallback(value); |
||||||
|
q.pop(); |
||||||
|
} |
||||||
|
pthread_mutex_unlock(&mutex); |
||||||
|
#endif |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
void sync() { |
||||||
|
#ifdef C11 |
||||||
|
lock_guard<mutex> lk(mt); |
||||||
|
syncHandle(q); |
||||||
|
#else |
||||||
|
pthread_mutex_lock(&mutex); |
||||||
|
syncHandle(q); |
||||||
|
pthread_mutex_unlock(&mutex); |
||||||
|
#endif |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
void setReleaseCallback(ReleaseCallback r) { |
||||||
|
releaseCallback = r; |
||||||
|
} |
||||||
|
|
||||||
|
void setSyncHandle(SyncHandle s) { |
||||||
|
syncHandle = s; |
||||||
|
} |
||||||
|
|
||||||
|
private: |
||||||
|
|
||||||
|
#ifdef C11 |
||||||
|
mutex mt; |
||||||
|
condition_variable cv; |
||||||
|
#else |
||||||
|
pthread_cond_t cond; |
||||||
|
pthread_mutex_t mutex; |
||||||
|
#endif |
||||||
|
|
||||||
|
queue<T> q; |
||||||
|
int work; |
||||||
|
ReleaseCallback releaseCallback; |
||||||
|
SyncHandle syncHandle; |
||||||
|
}; |
||||||
|
|
||||||
|
#endif |
@ -0,0 +1,162 @@ |
|||||||
|
package com.frank.live; |
||||||
|
|
||||||
|
import android.app.Activity; |
||||||
|
import android.view.SurfaceHolder; |
||||||
|
|
||||||
|
import com.frank.live.listener.LiveStateChangeListener; |
||||||
|
import com.frank.live.param.AudioParam; |
||||||
|
import com.frank.live.param.VideoParam; |
||||||
|
import com.frank.live.stream.AudioStream; |
||||||
|
import com.frank.live.stream.VideoStream; |
||||||
|
|
||||||
|
public class LivePusherNew { |
||||||
|
|
||||||
|
//视频编码器打开失败
|
||||||
|
private final static int ERROR_VIDEO_ENCODER_OPEN = 0x01; |
||||||
|
//视频帧编码失败
|
||||||
|
private final static int ERROR_VIDEO_ENCODE = 0x02; |
||||||
|
//音频编码器打开失败
|
||||||
|
private final static int ERROR_AUDIO_ENCODER_OPEN = 0x03; |
||||||
|
//音频帧编码失败
|
||||||
|
private final static int ERROR_AUDIO_ENCODE = 0x04; |
||||||
|
//RTMP连接失败
|
||||||
|
private final static int ERROR_RTMP_CONNECT = 0x05; |
||||||
|
//RTMP连接流失败
|
||||||
|
private final static int ERROR_RTMP_CONNECT_STREAM = 0x06; |
||||||
|
//RTMP发送数据包失败
|
||||||
|
private final static int ERROR_RTMP_SEND_PACKET = 0x07; |
||||||
|
|
||||||
|
static { |
||||||
|
System.loadLibrary("live"); |
||||||
|
} |
||||||
|
|
||||||
|
private AudioStream audioStream; |
||||||
|
private VideoStream videoStream; |
||||||
|
|
||||||
|
private LiveStateChangeListener liveStateChangeListener; |
||||||
|
|
||||||
|
public LivePusherNew(Activity activity, VideoParam videoParam, AudioParam audioParam) { |
||||||
|
native_init(); |
||||||
|
videoStream = new VideoStream(this, activity, videoParam.getWidth(), videoParam.getHeight(), |
||||||
|
videoParam.getBitRate(), videoParam.getFrameRate(), videoParam.getCameraId()); |
||||||
|
audioStream = new AudioStream(this, audioParam); |
||||||
|
} |
||||||
|
|
||||||
|
public void setPreviewDisplay(SurfaceHolder surfaceHolder) { |
||||||
|
videoStream.setPreviewDisplay(surfaceHolder); |
||||||
|
} |
||||||
|
|
||||||
|
public void switchCamera() { |
||||||
|
videoStream.switchCamera(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 设置静音 |
||||||
|
* |
||||||
|
* @param isMute 是否静音 |
||||||
|
*/ |
||||||
|
public void setMute(boolean isMute) { |
||||||
|
audioStream.setMute(isMute); |
||||||
|
} |
||||||
|
|
||||||
|
public void startPush(String path, LiveStateChangeListener stateChangeListener) { |
||||||
|
this.liveStateChangeListener = stateChangeListener; |
||||||
|
native_start(path); |
||||||
|
videoStream.startLive(); |
||||||
|
audioStream.startLive(); |
||||||
|
} |
||||||
|
|
||||||
|
public void stopPush() { |
||||||
|
videoStream.stopLive(); |
||||||
|
audioStream.stopLive(); |
||||||
|
native_stop(); |
||||||
|
} |
||||||
|
|
||||||
|
public void release() { |
||||||
|
videoStream.release(); |
||||||
|
audioStream.release(); |
||||||
|
native_release(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 当native报错时,回调这个方法 |
||||||
|
* |
||||||
|
* @param errCode errCode |
||||||
|
*/ |
||||||
|
public void errorFromNative(int errCode) { |
||||||
|
//停止推流
|
||||||
|
stopPush(); |
||||||
|
if (liveStateChangeListener != null) { |
||||||
|
String msg = ""; |
||||||
|
switch (errCode) { |
||||||
|
case ERROR_VIDEO_ENCODER_OPEN: |
||||||
|
msg = "视频编码器打开失败..."; |
||||||
|
break; |
||||||
|
case ERROR_VIDEO_ENCODE: |
||||||
|
msg = "视频帧编码失败..."; |
||||||
|
break; |
||||||
|
case ERROR_AUDIO_ENCODER_OPEN: |
||||||
|
msg = "音频编码器打开失败..."; |
||||||
|
break; |
||||||
|
case ERROR_AUDIO_ENCODE: |
||||||
|
msg = "音频帧编码失败..."; |
||||||
|
break; |
||||||
|
case ERROR_RTMP_CONNECT: |
||||||
|
msg = "RTMP连接失败..."; |
||||||
|
break; |
||||||
|
case ERROR_RTMP_CONNECT_STREAM: |
||||||
|
msg = "RTMP连接流失败..."; |
||||||
|
break; |
||||||
|
case ERROR_RTMP_SEND_PACKET: |
||||||
|
msg = "RTMP发送数据包失败..."; |
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
liveStateChangeListener.onError(msg); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void setVideoCodecInfo(int width, int height, int fps, int bitrate) { |
||||||
|
native_setVideoCodecInfo(width, height, fps, bitrate); |
||||||
|
} |
||||||
|
|
||||||
|
public void setAudioCodecInfo(int sampleRateInHz, int channels) { |
||||||
|
native_setAudioCodecInfo(sampleRateInHz, channels); |
||||||
|
} |
||||||
|
|
||||||
|
public void start(String path) { |
||||||
|
native_start(path); |
||||||
|
} |
||||||
|
|
||||||
|
public int getInputSample() { |
||||||
|
return getInputSamples(); |
||||||
|
} |
||||||
|
|
||||||
|
public void pushAudio(byte[] data) { |
||||||
|
native_pushAudio(data); |
||||||
|
} |
||||||
|
|
||||||
|
public void pushVideo(byte[] data) { |
||||||
|
native_pushVideo(data); |
||||||
|
} |
||||||
|
|
||||||
|
private native void native_init(); |
||||||
|
|
||||||
|
private native void native_start(String path); |
||||||
|
|
||||||
|
private native void native_setVideoCodecInfo(int width, int height, int fps, int bitrate); |
||||||
|
|
||||||
|
private native void native_setAudioCodecInfo(int sampleRateInHz, int channels); |
||||||
|
|
||||||
|
private native int getInputSamples(); |
||||||
|
|
||||||
|
private native void native_pushAudio(byte[] data); |
||||||
|
|
||||||
|
private native void native_pushVideo(byte[] data); |
||||||
|
|
||||||
|
private native void native_stop(); |
||||||
|
|
||||||
|
private native void native_release(); |
||||||
|
|
||||||
|
} |
@ -1,130 +0,0 @@ |
|||||||
package com.frank.live; |
|
||||||
|
|
||||||
import android.Manifest; |
|
||||||
import android.annotation.SuppressLint; |
|
||||||
import android.annotation.TargetApi; |
|
||||||
import android.media.AudioFormat; |
|
||||||
import android.os.Build; |
|
||||||
import android.os.Handler; |
|
||||||
import android.os.Message; |
|
||||||
|
|
||||||
import androidx.annotation.NonNull; |
|
||||||
import androidx.appcompat.app.AppCompatActivity; |
|
||||||
|
|
||||||
import android.os.Bundle; |
|
||||||
import android.text.TextUtils; |
|
||||||
import android.util.Log; |
|
||||||
import android.view.TextureView; |
|
||||||
import android.view.View; |
|
||||||
import android.widget.CompoundButton; |
|
||||||
import android.widget.Toast; |
|
||||||
import android.widget.ToggleButton; |
|
||||||
|
|
||||||
import com.frank.live.Push.LivePusher; |
|
||||||
import com.frank.live.camera2.Camera2Helper; |
|
||||||
import com.frank.live.listener.LiveStateChangeListener; |
|
||||||
import com.frank.live.param.AudioParam; |
|
||||||
import com.frank.live.param.VideoParam; |
|
||||||
|
|
||||||
/** |
|
||||||
* h264与rtmp实时推流直播 |
|
||||||
* Created by frank on 2018/1/28. |
|
||||||
*/ |
|
||||||
|
|
||||||
public class RtmpLiveActivity extends AppCompatActivity implements View.OnClickListener, CompoundButton.OnCheckedChangeListener, LiveStateChangeListener { |
|
||||||
|
|
||||||
private final static String TAG = RtmpLiveActivity.class.getSimpleName(); |
|
||||||
private final static int CODE_CAMERA_RECORD = 0x0001; |
|
||||||
private final static String[] permissions = new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO}; |
|
||||||
private final static String LIVE_URL = "rtmp://192.168.1.3/live/stream"; |
|
||||||
private final static int MSG_ERROR = 100; |
|
||||||
private TextureView textureView; |
|
||||||
private LivePusher livePusher; |
|
||||||
@SuppressLint("HandlerLeak") |
|
||||||
private Handler mHandler = new Handler() { |
|
||||||
@Override |
|
||||||
public void handleMessage(Message msg) { |
|
||||||
super.handleMessage(msg); |
|
||||||
if (msg.what == MSG_ERROR) { |
|
||||||
String errMsg = (String) msg.obj; |
|
||||||
if (!TextUtils.isEmpty(errMsg)) { |
|
||||||
Toast.makeText(RtmpLiveActivity.this, errMsg, Toast.LENGTH_SHORT).show(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
@Override |
|
||||||
protected void onCreate(Bundle savedInstanceState) { |
|
||||||
super.onCreate(savedInstanceState); |
|
||||||
setContentView(R.layout.activity_rtmp_live); |
|
||||||
|
|
||||||
initView(); |
|
||||||
requirePermission(); |
|
||||||
initPusher(); |
|
||||||
} |
|
||||||
|
|
||||||
private void initView() { |
|
||||||
findViewById(R.id.btn_swap).setOnClickListener(this); |
|
||||||
((ToggleButton) findViewById(R.id.btn_live)).setOnCheckedChangeListener(this); |
|
||||||
textureView = findViewById(R.id.surface_camera); |
|
||||||
} |
|
||||||
|
|
||||||
private void initPusher() { |
|
||||||
int width = 640;//分辨率设置很重要
|
|
||||||
int height = 480; |
|
||||||
int videoBitRate = 400;//kb/s jason-->480kb
|
|
||||||
int videoFrameRate = 25;//fps
|
|
||||||
VideoParam videoParam = new VideoParam(width, height, |
|
||||||
Integer.valueOf(Camera2Helper.CAMERA_ID_BACK), videoBitRate, videoFrameRate); |
|
||||||
int sampleRate = 44100;//采样率:Hz
|
|
||||||
int channelConfig = AudioFormat.CHANNEL_IN_STEREO;//立体声道
|
|
||||||
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;//pcm16位
|
|
||||||
int numChannels = 2;//声道数
|
|
||||||
AudioParam audioParam = new AudioParam(sampleRate, channelConfig, audioFormat, numChannels); |
|
||||||
livePusher = new LivePusher(textureView, videoParam, audioParam, this); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onClick(View v) { |
|
||||||
if (v.getId() == R.id.btn_swap) { |
|
||||||
livePusher.switchCamera(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { |
|
||||||
if (isChecked) { |
|
||||||
livePusher.startPush(LIVE_URL, this); |
|
||||||
} else { |
|
||||||
livePusher.stopPush(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onError(String msg) { |
|
||||||
Log.e(TAG, "errMsg=" + msg); |
|
||||||
mHandler.obtainMessage(MSG_ERROR, msg).sendToTarget(); |
|
||||||
} |
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.M) |
|
||||||
private void requirePermission() { |
|
||||||
requestPermissions(permissions, CODE_CAMERA_RECORD); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { |
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults); |
|
||||||
if (permissions.length > 0 && grantResults.length == permissions.length) { |
|
||||||
for (int i = 0; i < permissions.length; i++) { |
|
||||||
Log.i(TAG, permissions[i] + ":grantResult=" + grantResults[i]); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
protected void onDestroy() { |
|
||||||
super.onDestroy(); |
|
||||||
livePusher.release(); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,84 @@ |
|||||||
|
package com.frank.live.stream; |
||||||
|
|
||||||
|
import android.media.AudioFormat; |
||||||
|
import android.media.AudioRecord; |
||||||
|
import android.media.MediaRecorder; |
||||||
|
|
||||||
|
import com.frank.live.LivePusherNew; |
||||||
|
import com.frank.live.param.AudioParam; |
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService; |
||||||
|
import java.util.concurrent.Executors; |
||||||
|
|
||||||
|
public class AudioStream { |
||||||
|
|
||||||
|
private int inputSamples; |
||||||
|
private ExecutorService executor; |
||||||
|
private AudioRecord audioRecord; |
||||||
|
private LivePusherNew mLivePusher; |
||||||
|
private boolean isLiving; |
||||||
|
private boolean isMute; |
||||||
|
|
||||||
|
public AudioStream(LivePusherNew livePusher, AudioParam audioParam) { |
||||||
|
mLivePusher = livePusher; |
||||||
|
executor = Executors.newSingleThreadExecutor(); |
||||||
|
int channelConfig; |
||||||
|
if (audioParam.getNumChannels() == 2) { |
||||||
|
channelConfig = AudioFormat.CHANNEL_IN_STEREO; |
||||||
|
} else { |
||||||
|
channelConfig = AudioFormat.CHANNEL_IN_MONO; |
||||||
|
} |
||||||
|
|
||||||
|
mLivePusher.setAudioCodecInfo(audioParam.getSampleRate(), audioParam.getNumChannels()); |
||||||
|
inputSamples = mLivePusher.getInputSample() * 2; |
||||||
|
|
||||||
|
int minBufferSize = AudioRecord.getMinBufferSize(audioParam.getSampleRate(), |
||||||
|
channelConfig, audioParam.getAudioFormat()) * 2; |
||||||
|
int bufferSizeInBytes = minBufferSize > inputSamples ? minBufferSize : inputSamples; |
||||||
|
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, audioParam.getSampleRate(), |
||||||
|
channelConfig, audioParam.getAudioFormat(), bufferSizeInBytes); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public void startLive() { |
||||||
|
isLiving = true; |
||||||
|
executor.submit(new AudioTask()); |
||||||
|
} |
||||||
|
|
||||||
|
public void stopLive() { |
||||||
|
isLiving = false; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public void release() { |
||||||
|
audioRecord.release(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
class AudioTask implements Runnable { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
audioRecord.startRecording(); |
||||||
|
byte[] bytes = new byte[inputSamples]; |
||||||
|
while (isLiving) { |
||||||
|
if (!isMute) { |
||||||
|
int len = audioRecord.read(bytes, 0, bytes.length); |
||||||
|
if (len > 0) { |
||||||
|
mLivePusher.pushAudio(bytes); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
audioRecord.stop(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 设置静音 |
||||||
|
* @param isMute 是否静音 |
||||||
|
*/ |
||||||
|
public void setMute(boolean isMute){ |
||||||
|
this.isMute = isMute; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,217 @@ |
|||||||
|
package com.frank.live.stream; |
||||||
|
|
||||||
|
import android.app.Activity; |
||||||
|
import android.graphics.ImageFormat; |
||||||
|
import android.hardware.Camera; |
||||||
|
import android.view.Surface; |
||||||
|
import android.view.SurfaceHolder; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
public class CameraHelper implements SurfaceHolder.Callback, Camera.PreviewCallback { |
||||||
|
|
||||||
|
private Activity mActivity; |
||||||
|
private int mHeight; |
||||||
|
private int mWidth; |
||||||
|
private int mCameraId; |
||||||
|
private Camera mCamera; |
||||||
|
private byte[] buffer; |
||||||
|
private SurfaceHolder mSurfaceHolder; |
||||||
|
private Camera.PreviewCallback mPreviewCallback; |
||||||
|
private int mRotation; |
||||||
|
private OnChangedSizeListener mOnChangedSizeListener; |
||||||
|
private byte[] bytes; |
||||||
|
|
||||||
|
CameraHelper(Activity activity, int cameraId, int width, int height) { |
||||||
|
mActivity = activity; |
||||||
|
mCameraId = cameraId; |
||||||
|
mWidth = width; |
||||||
|
mHeight = height; |
||||||
|
} |
||||||
|
|
||||||
|
void switchCamera() { |
||||||
|
if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) { |
||||||
|
mCameraId = Camera.CameraInfo.CAMERA_FACING_FRONT; |
||||||
|
} else { |
||||||
|
mCameraId = Camera.CameraInfo.CAMERA_FACING_BACK; |
||||||
|
} |
||||||
|
stopPreview(); |
||||||
|
startPreview(); |
||||||
|
} |
||||||
|
|
||||||
|
private void stopPreview() { |
||||||
|
if (mCamera != null) { |
||||||
|
//预览数据回调接口
|
||||||
|
mCamera.setPreviewCallback(null); |
||||||
|
//停止预览
|
||||||
|
mCamera.stopPreview(); |
||||||
|
//释放摄像头
|
||||||
|
mCamera.release(); |
||||||
|
mCamera = null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void startPreview() { |
||||||
|
try { |
||||||
|
mCamera = Camera.open(mCameraId); |
||||||
|
Camera.Parameters parameters = mCamera.getParameters(); |
||||||
|
parameters.setPreviewFormat(ImageFormat.NV21); |
||||||
|
setPreviewSize(parameters); |
||||||
|
setPreviewOrientation(parameters); |
||||||
|
mCamera.setParameters(parameters); |
||||||
|
buffer = new byte[mWidth * mHeight * 3 / 2]; |
||||||
|
bytes = new byte[buffer.length]; |
||||||
|
mCamera.addCallbackBuffer(buffer); |
||||||
|
mCamera.setPreviewCallbackWithBuffer(this); |
||||||
|
mCamera.setPreviewDisplay(mSurfaceHolder); |
||||||
|
mCamera.startPreview(); |
||||||
|
} catch (Exception ex) { |
||||||
|
ex.printStackTrace(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void setPreviewOrientation(Camera.Parameters parameters) { |
||||||
|
Camera.CameraInfo info = new Camera.CameraInfo(); |
||||||
|
Camera.getCameraInfo(mCameraId, info); |
||||||
|
mRotation = mActivity.getWindowManager().getDefaultDisplay().getRotation(); |
||||||
|
int degrees = 0; |
||||||
|
switch (mRotation) { |
||||||
|
case Surface.ROTATION_0: |
||||||
|
degrees = 0; |
||||||
|
mOnChangedSizeListener.onChanged(mHeight, mWidth); |
||||||
|
break; |
||||||
|
case Surface.ROTATION_90: |
||||||
|
degrees = 90; |
||||||
|
mOnChangedSizeListener.onChanged(mWidth, mHeight); |
||||||
|
break; |
||||||
|
case Surface.ROTATION_270: |
||||||
|
degrees = 270; |
||||||
|
mOnChangedSizeListener.onChanged(mWidth, mHeight); |
||||||
|
break; |
||||||
|
} |
||||||
|
int result; |
||||||
|
if (info.facing == Camera.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; |
||||||
|
} |
||||||
|
mCamera.setDisplayOrientation(result); |
||||||
|
} |
||||||
|
|
||||||
|
private void setPreviewSize(Camera.Parameters parameters) { |
||||||
|
List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes(); |
||||||
|
Camera.Size size = supportedPreviewSizes.get(0); |
||||||
|
//选择最合适的camera支持分辨率
|
||||||
|
int m = Math.abs(size.height * size.width - mWidth * mHeight); |
||||||
|
supportedPreviewSizes.remove(0); |
||||||
|
for (Camera.Size next : supportedPreviewSizes) { |
||||||
|
int n = Math.abs(next.height * next.width - mWidth * mHeight); |
||||||
|
if (n < m) { |
||||||
|
m = n; |
||||||
|
size = next; |
||||||
|
} |
||||||
|
} |
||||||
|
mWidth = size.width; |
||||||
|
mHeight = size.height; |
||||||
|
parameters.setPreviewSize(mWidth, mHeight); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
void setPreviewDisplay(SurfaceHolder surfaceHolder) { |
||||||
|
mSurfaceHolder = surfaceHolder; |
||||||
|
mSurfaceHolder.addCallback(this); |
||||||
|
} |
||||||
|
|
||||||
|
void setPreviewCallback(Camera.PreviewCallback previewCallback) { |
||||||
|
mPreviewCallback = previewCallback; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void surfaceCreated(SurfaceHolder holder) { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { |
||||||
|
stopPreview(); |
||||||
|
startPreview(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void surfaceDestroyed(SurfaceHolder holder) { |
||||||
|
stopPreview(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void onPreviewFrame(byte[] data, Camera camera) { |
||||||
|
switch (mRotation) { |
||||||
|
case Surface.ROTATION_0: |
||||||
|
rotation90(data); |
||||||
|
break; |
||||||
|
case Surface.ROTATION_90: |
||||||
|
break; |
||||||
|
case Surface.ROTATION_270: |
||||||
|
break; |
||||||
|
} |
||||||
|
mPreviewCallback.onPreviewFrame(bytes, camera); |
||||||
|
camera.addCallbackBuffer(buffer); |
||||||
|
} |
||||||
|
|
||||||
|
private void rotation90(byte[] data) { |
||||||
|
int index = 0; |
||||||
|
int ySize = mWidth * mHeight; |
||||||
|
int uvHeight = mHeight / 2; |
||||||
|
//后置摄像头顺时针旋转90度
|
||||||
|
if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) { |
||||||
|
|
||||||
|
for (int i = 0; i < mWidth; i++) { |
||||||
|
for (int j = mHeight - 1; j >= 0; j--) { |
||||||
|
bytes[index++] = data[mWidth * j + i]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for (int i = 0; i < mWidth; i += 2) { |
||||||
|
for (int j = uvHeight - 1; j >= 0; j--) { |
||||||
|
// v
|
||||||
|
bytes[index++] = data[ySize + mWidth * j + i]; |
||||||
|
// u
|
||||||
|
bytes[index++] = data[ySize + mWidth * j + i + 1]; |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
//逆时针旋转90度
|
||||||
|
for (int i = 0; i < mWidth; i++) { |
||||||
|
int nPos = mWidth - 1; |
||||||
|
for (int j = 0; j < mHeight; j++) { |
||||||
|
bytes[index++] = data[nPos - i]; |
||||||
|
nPos += mWidth; |
||||||
|
} |
||||||
|
} |
||||||
|
//u v
|
||||||
|
for (int i = 0; i < mWidth; i += 2) { |
||||||
|
int nPos = ySize + mWidth - 1; |
||||||
|
for (int j = 0; j < uvHeight; j++) { |
||||||
|
bytes[index++] = data[nPos - i - 1]; |
||||||
|
bytes[index++] = data[nPos - i]; |
||||||
|
nPos += mWidth; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
void setOnChangedSizeListener(OnChangedSizeListener listener) { |
||||||
|
mOnChangedSizeListener = listener; |
||||||
|
} |
||||||
|
|
||||||
|
public void release() { |
||||||
|
mSurfaceHolder.removeCallback(this); |
||||||
|
stopPreview(); |
||||||
|
} |
||||||
|
|
||||||
|
public interface OnChangedSizeListener { |
||||||
|
void onChanged(int w, int h); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
package com.frank.live.stream; |
||||||
|
|
||||||
|
import android.app.Activity; |
||||||
|
import android.hardware.Camera; |
||||||
|
import android.view.SurfaceHolder; |
||||||
|
|
||||||
|
import com.frank.live.LivePusherNew; |
||||||
|
|
||||||
|
|
||||||
|
public class VideoStream implements Camera.PreviewCallback, CameraHelper.OnChangedSizeListener { |
||||||
|
|
||||||
|
|
||||||
|
private LivePusherNew mLivePusher; |
||||||
|
private CameraHelper cameraHelper; |
||||||
|
private int mBitrate; |
||||||
|
private int mFps; |
||||||
|
private boolean isLiving; |
||||||
|
|
||||||
|
public VideoStream(LivePusherNew livePusher, Activity activity, int width, int height, int bitrate, int fps, int cameraId) { |
||||||
|
mLivePusher = livePusher; |
||||||
|
mBitrate = bitrate; |
||||||
|
mFps = fps; |
||||||
|
cameraHelper = new CameraHelper(activity, cameraId, width, height); |
||||||
|
cameraHelper.setPreviewCallback(this); |
||||||
|
cameraHelper.setOnChangedSizeListener(this); |
||||||
|
} |
||||||
|
|
||||||
|
public void setPreviewDisplay(SurfaceHolder surfaceHolder) { |
||||||
|
cameraHelper.setPreviewDisplay(surfaceHolder); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* nv21摄像头数据 |
||||||
|
* |
||||||
|
* @param data data |
||||||
|
* @param camera camera |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public void onPreviewFrame(byte[] data, Camera camera) { |
||||||
|
if (isLiving) { |
||||||
|
mLivePusher.pushVideo(data); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void switchCamera() { |
||||||
|
cameraHelper.switchCamera(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onChanged(int w, int h) { |
||||||
|
mLivePusher.setVideoCodecInfo(w, h, mFps, mBitrate); |
||||||
|
} |
||||||
|
|
||||||
|
public void startLive() { |
||||||
|
isLiving = true; |
||||||
|
} |
||||||
|
|
||||||
|
public void stopLive() { |
||||||
|
isLiving = false; |
||||||
|
} |
||||||
|
|
||||||
|
public void release() { |
||||||
|
cameraHelper.release(); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue