源码地址
https://github.com/979451341/Rtmp
1. 配置 RTMP 服务器
这个我不多说贴两个博客分别是在 mac 和 windows 环境上的, 大家跟着弄
MAC 搭建 RTMP 服务器
https://www.jianshu.com/p/6fcec3b9d644
这个是在 windows 上的, RTMP 服务器搭建 (crtmpserver 和 nginx)
https://www.jianshu.com/p/c71cc39f72ec
2. 关于推流输出的 ip 地址我好好说说
我这里是手机开启热点, 电脑连接手机, 这个 RTMP 服务器的推流地址有 localhost, 服务器在电脑上, 对于电脑这个 localhost 是 127.0.0.1, 但是对于外界比如手机, 你不能用 localhost, 而是用这个电脑的在这个热点也就是局域网的 ip 地址, 不是 127.0.0.1 这个只代表本设备节点的 ip 地址, 这个你需要去手机设置更多移动网络共享便携式 WLAN 热点管理设备列表, 就可以看到电脑的局域网 ip 地址了
3. 说说代码
注册组件, 第二个如果不加的话就不能获取网络信息, 比如类似 url
- av_register_all();
- avformat_network_init();
获取输入视频的信息, 和创建输出 url 地址的环境
- av_dump_format(ictx, 0, inUrl, 0);
- ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
- if (ret < 0) {
- avError(ret);
- throw ret;
- }
将输入视频流放入刚才创建的输出流里
- for (i = 0; i < ictx->nb_streams; i++) {
- // 获取输入视频流
- AVStream *in_stream = ictx->streams[i];
- // 为输出上下文添加音视频流 (初始化一个音视频流容器)
- AVStream *out_stream = avformat_new_stream(octx, in_stream->codec->codec);
- if (!out_stream) {
- printf("未能成功添加音视频流 \ n");
- ret = AVERROR_UNKNOWN;
- }
- if (octx->oformat->flags & AVFMT_GLOBALHEADER) {
- out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
- }
- ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
- if (ret < 0) {
- printf("copy 编解码器上下文失败 \ n");
- }
- out_stream->codecpar->codec_tag = 0;
- // out_stream->codec->codec_tag = 0;
- }
打开输出 url, 并写入头部数据
- // 打开 IO
- ret = avio_open(&octx->pb, outUrl, AVIO_FLAG_WRITE);
- if (ret < 0) {
- avError(ret);
- throw ret;
- }
- logd("avio_open success!");
- // 写入头部信息
- ret = avformat_write_header(octx, 0);
- if (ret < 0) {
- avError(ret);
- throw ret;
- }
然后开始循环解码并推流数据
首先获取一帧的数据
ret = av_read_frame(ictx, &pkt);
然后给这一帧的数据配置参数, 如果原有配置没有时间就配置时间, 我在这里再提两个概念
DTS(解码时间戳) 和 PTS(显示时间戳) 分别是解码器进行解码和显示帧时相对于 SCR(系统参考) 的时间戳 SCR 可以理解为解码器应该开始从磁盘读取数据时的时间
- if (pkt.pts == AV_NOPTS_VALUE) {
- //AVRational time_base: 时基通过该值可以把 PTS,DTS 转化为真正的时间
- AVRational time_base1 = ictx->streams[videoindex]->time_base;
- int64_t calc_duration =
- (double) AV_TIME_BASE / av_q2d(ictx->streams[videoindex]->r_frame_rate);
- // 配置参数
- pkt.pts = (double) (frame_index * calc_duration) /
- (double) (av_q2d(time_base1) * AV_TIME_BASE);
- pkt.dts = pkt.pts;
- pkt.duration =
- (double) calc_duration / (double) (av_q2d(time_base1) * AV_TIME_BASE);
- }
调节播放时间, 就是当初我们解码视频之前记录了一个当前时间, 然后在循环推流的时候又获取一次当前时间, 两者的差值是我们视频应该播放的时间, 如果视频播放太快就进程休眠 pkt.dts 减去实际播放的时间的差值
- if (pkt.stream_index == videoindex) {
- AVRational time_base = ictx->streams[videoindex]->time_base;
- AVRational time_base_q = {1, AV_TIME_BASE};
- // 计算视频播放时间
- int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
- // 计算实际视频的播放时间
- int64_t now_time = av_gettime() - start_time;
- AVRational avr = ictx->streams[videoindex]->time_base;
- cout << avr.num << "" << avr.den <<" "<< pkt.dts <<" "<< pkt.pts <<" "
- << pts_time << endl;
- if (pts_time > now_time) {
- // 睡眠一段时间 (目的是让当前视频记录的播放时间与实际时间同步)
- av_usleep((unsigned int) (pts_time - now_time));
- }
- }
如果延时了, 这一帧的配置所记录的时间就应该改变
- // 计算延时后, 重新指定时间戳
- pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base,
- (AVRounding) (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
- pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base,
- (AVRounding) (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
- pkt.duration = (int) av_rescale_q(pkt.duration, in_stream->time_base,
- out_stream->time_base);
回调这一帧的时间参数, 这里在 MainActivity 里实例化了接口, 显示播放时间
- int res = FFmpegHandle.setCallback(new PushCallback() {
- @Override
- public void videoCallback(final long pts, final long dts, final long duration, final long index) {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- if(pts == -1){
- tvPushInfo.setText("播放结束");
- return ;
- }
- tvPushInfo.setText("播放时间:"+dts/1000+"秒");
- }
- });
- }
- });
然后段代码调用了 c 语言的 setCallback 函数, 获取了接口的实例, 和接口的 videoCallback 函数引用, 这里还调用了一次这个函数初始化时间显示
- // 转换为全局变量
- pushCallback = env->NewGlobalRef(pushCallback1);
- if (pushCallback == NULL) {
- return -3;
- }
- cls = env->GetObjectClass(pushCallback);
- if (cls == NULL) {
- return -1;
- }
- mid = env->GetMethodID(cls, "videoCallback", "(JJJJ)V");
- if (mid == NULL) {
- return -2;
- }
- env->CallVoidMethod(pushCallback, mid, (jlong) 0, (jlong) 0, (jlong) 0, (jlong) 0);
这个时候我们回到循环推流一帧帧数据的时候调用 videoCallback 函数
- env->CallVoidMethod(pushCallback, mid, (jlong) pts, (jlong) dts, (jlong) duration,
- (jlong) index);
然后就是向输出 url 输出数据, 并释放这一帧的数据
- ret = av_interleaved_write_frame(octx, &pkt);
- av_packet_unref(&pkt);
释放资源
- // 关闭输出上下文, 这个很关键
- if (octx != NULL)
- avio_close(octx->pb);
- // 释放输出封装上下文
- if (octx != NULL)
- avformat_free_context(octx);
- // 关闭输入上下文
- if (ictx != NULL)
- avformat_close_input(&ictx);
- octx = NULL;
- ictx = NULL;
- env->ReleaseStringUTFChars(path_, path);
- env->ReleaseStringUTFChars(outUrl_, outUrl);
最后回调时间显示, 说播放结束
callback(env, -1, -1, -1, -1);
4. 关于接收推流数据
我这里使用的是 VLC, 这个 mac 和 windows 都有版本, FILEOPEN NETWORK, 输入之前的输出 url 就可以了这里要注意首先在 app 上开启推流再使用 VLC 打开 url 才可以
效果如下
参考文章
https://www.jianshu.com/p/dcac5da8f1da
这个博主对于推流真的熟练, 大家如果对推流还想输入了解可以看看他的博客
来源: http://blog.csdn.net/z979451341/article/details/79392386