Push-поток Nginx-RTMP (видео)

Java сервер Nginx C++

Камера собирает данные

Камера отвечает за сбор данных, передачу собранных данных в X264 для кодирования и упаковки в RTMP для потоковой передачи,

Данные, собираемые камерой, имеют формат NV21, а формат входных данных, закодированный X264, — формат I420.

И NV21, и I420 относятся к формату YUV420. NV21 — двухплоскостной режим, то есть Y и UV разделены на две Planes (плоскости), а вот UV (CbCr) хранится чередующимися, 2 плоскости вместо трех. Эта компоновка называется YUV420SP, а I420 называется YUV420P. (Y: яркость, оттенки серого, UV: оттенок, насыщенность)

На следующем рисунке показаны данные NV21 размером 4x4: Y1, Y2, Y5, Y6 разделяют V1 и U1,...

И I420 есть

Можно видеть, что независимо от того, какое расположение используется, объем данных YUV420 составляет: w*h+w/2*h/2+w/2*h/2 равно w*h*3/2.

Преобразование NV21 в I420 это:

Данные Y копируются полностью последовательно, а данные U добавляются на один байт после всех данных Y, а затем берутся через каждый второй байт.

Если датчик не соответствует естественной ориентации экрана, поверните систему координат датчика изображения на 90 градусов против часовой стрелки, чтобы отобразить его в системе координат экрана. Таким образом, изображение, которое мы видим, повернуто на 90 градусов против часовой стрелки, поэтому нам нужно повернуть изображение на 90 градусов по часовой стрелке, чтобы увидеть нормальное изображение. А объект Camera предоставляетsetDisplayOrientationВ интерфейсе можно задать угол отображения превью:

Согласно документации превью действительно нормальное после настройки Камеры, но данные полученные обратным вызовом в onPreviewFrame все равно повернуты на 90 градусов против часовой стрелки. Поэтому, если вам нужно использовать данные обратного вызова предварительного просмотра, вам также необходимо повернуть byte[] обратного вызова onPreviewFrame.

То есть данные NV21 повернуты на 90 градусов по часовой стрелке.

Инициализировать кодировщик, поставить SafeQueue в очередь

Камера передает данные byte[] data нативному через PreviewCallBack. Native подготавливает кодировку кодировщика при инициализации, для хранения данных используется очередь, кодировщик x264_t *videoCodec = 0; хранится в VideoChannel.cpp

//native-lib.cpp 文件
//队列
SafeQueue<RTMPPacket *> packets;
VideoChannel *videoChannel = 0;

extern "C"
JNIEXPORT void JNICALL
Java_com_tina_pushstream_live_LivePusher_native_1init(JNIEnv *env, jobject instance) {
    //准备一个Video编码器的工具类 :进行编码
    videoChannel = new VideoChannel;
    videoChannel->setVideoCallback(callback);
    //准备一个队列,打包好的数据 放入队列,在线程中统一的取出数据再发送给服务器
    packets.setReleaseCallback(releasePackets);
}

Создайте кодировщик в VideoChannel и задайте параметры:

//  VideoChannel.h/VideoChannel.cpp
x264_t *videoCodec = 0;

//设置编码器参数
void VideoChannel::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;
        pic_in = 0;
    }

    //打开x264编码器
    //x264编码器的属性
    x264_param_t param;
    //2: 最快
    //3:  无延迟编码
    x264_param_default_preset(&param, "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(&param, "baseline");
    //打开编码器 videoCodec
    videoCodec = x264_encoder_open(&param);
    pic_in = new x264_picture_t;
    x264_picture_alloc(pic_in, X264_CSP_I420, width, height);
    pthread_mutex_unlock(&mutex);
}

#подключить сервис

native_start запускает поток для подключения к серверу.RTMP, как и Http, является протоколом верхнего уровня, основанным на TCP, поэтому он подключается в методе запуска.

//LivePusher 调用native_start()
public void startLive(String path) {
        native_start(path);
        videoChannel.startLive();
        audioChannel.startLive();
 }

RTMP собственного уровня подключается к серверу, сначала запускает поток и открывает соединение в обратном вызове потока:

//native-lib.cpp

extern "C"
JNIEXPORT void JNICALL
Java_com_dongnao_pusher_live_LivePusher_native_1start(JNIEnv *env, jobject instance,
                                                      jstring path_) {
    if (isStart) {
        return;
    }
    const char *path = env->GetStringUTFChars(path_, 0);
    char *url = new char[strlen(path) + 1];
    strcpy(url, path);
    isStart = 1;
    //启动线程
    pthread_create(&pid, 0, start, url);
    env->ReleaseStringUTFChars(path_, path);
}

//线程启动 RTMP connect 服务器
void *start(void *args) {
    char *url = static_cast<char *>(args);
    RTMP *rtmp = 0;
    do {
        rtmp = RTMP_Alloc();
        if (!rtmp) {
            LOGE("rtmp创建失败");
            break;
        }
        RTMP_Init(rtmp);
        //设置超时时间 5s
        rtmp->Link.timeout = 5;
        int ret = RTMP_SetupURL(rtmp, url);
        if (!ret) {
            LOGE("rtmp设置地址失败:%s", url);
            break;
        }
        //开启输出模式
        RTMP_EnableWrite(rtmp);
        ret = RTMP_Connect(rtmp, 0);
        if (!ret) {
            LOGE("rtmp连接地址失败:%s", url);
            break;
        }
        ret = RTMP_ConnectStream(rtmp, 0);
        if (!ret) {
            LOGE("rtmp连接流失败:%s", url);
            break;
        }

        //准备好了 可以开始推流了
        readyPushing = 1;
        //记录一个开始推流的时间
        start_time = RTMP_GetTime();
        packets.setWork(1);
        RTMPPacket *packet = 0;
        //循环从队列取包 然后发送
        while (isStart) {
            packets.pop(packet);
            if (!isStart) {
                break;
            }
            if (!packet) {
                continue;
            }
            // 给rtmp的流id
            packet->m_nInfoField2 = rtmp->m_stream_id;
            //发送包 1:加入队列发送
            ret = RTMP_SendPacket(rtmp, packet, 1);
            releasePackets(packet);
            if (!ret) {
                LOGE("发送数据失败");
                break;
            }
        }
        releasePackets(packet);
    } while (0);
    if (rtmp) {
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
    }
    delete url;
    return 0;
}

Весь процесс в функции запуска выше:

передача информации

После того, как начальное соединение завершено, данные pushVideo запускаются:

//VideoChannel,  在LivePusher中start时调用 videoChannel.startLive()
public void startLive() {
    isLiving = true;
}

//在 PreviewCallback中的回调里,此时isLiving为true,调用native_pushVideo.
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
  if (isLiving) {
    mLivePusher.native_pushVideo(data);
  }
}

I420 из NV21, захваченный камерой, в X264 необходимо перекодировать:

extern "C"
JNIEXPORT void JNICALL
Java_com_tina_pushstream_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);
}

Согласно разным форматам yuv NV21 и I420, после конвертации сохраняется в x264_picture_t *pic_in=0;

//图片
x264_picture_t *pic_in = 0;

//编码,把NV21 转成I420
void VideoChannel::encodeData(int8_t *data) {
    //编码
    pthread_mutex_lock(&mutex);
    //将data 放入 pic_in
    //y数据
    memcpy(pic_in->img.plane[0], data, ySize);
    for (int i = 0; i < uvSize; ++i) {
        //间隔1个字节取一个数据
        //u数据
        *(pic_in->img.plane[1] + i) = *(data + ySize + i * 2 + 1);
        //v数据
        *(pic_in->img.plane[2] + i) = *(data + ySize + i * 2);
    }
    pic_in->i_pts = index++;
    //编码出的数据
    x264_nal_t *pp_nal;
    //编码出了几个 nalu (暂时理解为帧)
    int pi_nal;
    x264_picture_t pic_out;
    //编码
    int ret = x264_encoder_encode(videoCodec, &pp_nal, &pi_nal, pic_in, &pic_out);
    if (ret < 0) {
        pthread_mutex_unlock(&mutex);
        return;
    }
    int sps_len, 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) {
            // 去掉 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].i_payload,pp_nal[i].p_payload);
        }
    }
    pthread_mutex_unlock(&mutex);
}

Соберите кадры spspps, кадры:

//拼数据,省略了数据拼装的过程
void VideoChannel::sendSpsPps(uint8_t *sps, uint8_t *pps, int sps_len, int pps_len) {
    RTMPPacket *packet = new RTMPPacket;
    int bodysize = 13 + sps_len + 3 + pps_len;
    RTMPPacket_Alloc(packet, bodysize);
    int i = 0;
    //固定头
    packet->m_body[i++] = 0x17;
    ......
    ......
    //sps pps没有时间戳
    packet->m_nTimeStamp = 0;
    //不使用绝对时间
    packet->m_hasAbsTimestamp = 0;
    packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;

    callback(packet);
}

void VideoChannel::sendFrame(int type, int payload, uint8_t *p_payload) {
    //去掉 00 00 00 01 / 00 00 01
    if (p_payload[2] == 0x00){
        payload -= 4;
        p_payload += 4;
    } else if(p_payload[2] == 0x01){
        payload -= 3;
        p_payload += 3;
    }
    RTMPPacket *packet = new RTMPPacket;
    int bodysize = 9 + payload;
    .........
    .......
    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nChannel = 0x10;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    //通过函数
    callback(packet);
}

Наконец, пакет помещается в очередь через указатель функции:

//native-lib.cpp
void callback(RTMPPacket *packet) {
    if (packet) {
        //设置时间戳
        packet->m_nTimeStamp = RTMP_GetTime() - start_time;
        //这里往队列里 塞数据,在start中 pop取数据然后发出去
        packets.push(packet);
    }
}

Использование очереди завершается при успешном стартовом соединении и завершении всего процесса загрузки видео.

 //循环从队列取包 然后发送
        while (isStart) {
            packets.pop(packet);
            if (!isStart) {
                break;
            }
            if (!packet) {
                continue;
            }
            // 给rtmp的流id
            packet->m_nInfoField2 = rtmp->m_stream_id;
            //发送包 1:加入队列发送
            ret = RTMP_SendPacket(rtmp, packet, 1);
            releasePackets(packet);
            if (!ret) {
                LOGE("发送数据失败");
                break;
            }
        }
        releasePackets(packet);

результат операции

VLC обращается к серверу для потокового воспроизведения: