Android平台下使用FFmpeg进行RTMP推流(视频文件推流)

简介

前面已经讲到如何在Linux环境下编译FFmpeg以及在Android项目中使用,这一节就开始真正的使用FFmpeg。在Android平台下用FFmepg解析视频文件并进行RTMP推流。如果对FFmpeg基础不熟或者不知道如何在Android项目中使用,请先阅读流媒体专栏里之前的文章。
注意:这里的工程沿用Linux下FFmpeg编译以及Android平台下使用里的工程和结构。

  • 新增推流函数
  • 异常处理
  • 设置回调方法
  • 常见问题
  • 源码

新增推流函数

首先我们将所有FFmpeg的操作抽取到一个类里面,然后增加推流方法。

public class FFmpegHandle {
    private static FFmpegHandle mInstance;

    public static void init(Context context) {
        mInstance = new FFmpegHandle();
    }

    public static FFmpegHandle getInstance() {
        if (mInstance == null) {
            throw new RuntimeException("FFmpegHandle must init fist");
        }
        return mInstance;
    }

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("avutil-55");
        System.loadLibrary("swresample-2");
        System.loadLibrary("avcodec-57");
        System.loadLibrary("avformat-57");
        System.loadLibrary("swscale-4");
        System.loadLibrary("avfilter-6");
        System.loadLibrary("avdevice-57");
        System.loadLibrary("ffmpeg-handle");
    }
    public native int setCallback(PushCallback pushCallback);

    public native String getAvcodecConfiguration();

    public native int pushRtmpFile(String path);
}

我们先看到public native int pushRtmpFile(String path);方法,这里主要传入的参数是文件的路径。然后在cpp层的代码中也增加方法

JNIEXPORT jint JNICALL
Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_pushRtmpFile(JNIEnv *env, jobject instance,
                                                             jstring path_) {
...省略代码
}

接下来就到了cpp层的开发,基本上和基于FFmpeg进行RTMP推流(二)中使用的代码一致,我们直接拷贝过来即可。至于FFmpeg的使用,这里就不重复讲了,不懂的可以看之前的文章。源码见末尾

异常处理

在我们之前的推流代码中,并没有做异常处理。这样在正式的使用中肯定不太好的。所以我们加上try catch。统一进行资源释放。源码见末尾

设置回调方法

为了方便我们查看推流的信息,我们新增一个回调类。

  • FFmpegHandle增加本地调用方法
public native int setCallback(PushCallback pushCallback);
  • 同样cpp层也需要增加对应函数
/**
 * 设置回到对象
 */
extern "C"
JNIEXPORT jint JNICALL
Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_setCallback(JNIEnv *env, jobject instance,
                                                            jobject pushCallback1) {
    //转换为全局变量
    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);
    return 0;
}

这里有个重点,就是对于传递进来的对象jobject pushCallback1是局部变量。而我们需要在推流的时候使用到这个对象,所以需要转化成全局变量

pushCallback = env->NewGlobalRef(pushCallback1);

同样也需要定义对应的全局变量

jobject pushCallback = NULL;
jclass cls = NULL;
jmethodID mid = NULL;

GetObjectClass、GetMethodID、CallVoidMethod这几个方法看名称也知道其功能。我们在设置回到对象时候就讲方法获取出来,后面就不需要每次去查找。

    cls = env->GetObjectClass(pushCallback);
    if (cls == NULL) {
        return -1;
    }
    mid = env->GetMethodID(cls, "videoCallback", "(JJJJ)V");
    if (mid == NULL) {
        return -2;
    }
  • 定义一个本地回调方法
int callback(JNIEnv *env, int64_t pts, int64_t dts, int64_t duration, long long index) {
//    logw("=================")
    if (pushCallback == NULL) {
        return -3;
    }
    if (cls == NULL) {
        return -1;
    }
    if (mid == NULL) {
        return -2;
    }
    env->CallVoidMethod(pushCallback, mid, (jlong) pts, (jlong) dts, (jlong) duration,
                        (jlong) index);
    return 0;
}

这样我们在推流的过程中就可以调用callback函数,将数据回调到java层。

//回调数据
callback(env, pkt.pts, pkt.dts, pkt.duration, frame_index);
  • java层设置回调对象
        int res = FFmpegHandle.getInstance().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() {
                        StringBuilder sb = new StringBuilder();
                        sb.append("pts: ").append(pts).append("\n");
                        sb.append("dts: ").append(dts).append("\n");
                        sb.append("duration: ").append(duration).append("\n");
                        sb.append("index: ").append(index).append("\n");
                        tvPushInfo.setText(sb.toString());
                    }
                });
            }
        });

常见问题

第二次推流出现Operation not permitted
这是因为第一次推流完没有关闭输出上下文

    //关闭输出上下文,这个很关键。Linux下FFmpeg编译以及Android平台下使用
    if (octx != NULL)
        avio_close(octx->pb);

加上即可。

源码

Github源码地址注意下载对应的版本

9.png

FFmpegHandle.java

public class FFmpegHandle {
    private static FFmpegHandle mInstance;

    public static void init(Context context) {
        mInstance = new FFmpegHandle();
    }

    public static FFmpegHandle getInstance() {
        if (mInstance == null) {
            throw new RuntimeException("FFmpegHandle must init fist");
        }
        return mInstance;
    }

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("avutil-55");
        System.loadLibrary("swresample-2");
        System.loadLibrary("avcodec-57");
        System.loadLibrary("avformat-57");
        System.loadLibrary("swscale-4");
        System.loadLibrary("avfilter-6");
        System.loadLibrary("avdevice-57");
        System.loadLibrary("ffmpeg-handle");
    }
    public native int setCallback(PushCallback pushCallback);

    public native String getAvcodecConfiguration();

    public native int pushRtmpFile(String path);
}

MainActivity.java

package com.wangheart.rtmpfile;

import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

import com.wangheart.rtmpfile.ffmpeg.FFmpegHandle;
import com.wangheart.rtmpfile.ffmpeg.PushCallback;

import java.io.File;

public class MainActivity extends Activity {
    private TextView tvCodecInfo;
    private TextView tvPushInfo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initData();
    }

    private void initView() {
        tvCodecInfo = findViewById(R.id.tv_codec_info);
        tvPushInfo = findViewById(R.id.tv_push_info);
    }


    private void initData() {
        FFmpegHandle.init(this);
        tvCodecInfo.setText(FFmpegHandle.getInstance().getAvcodecConfiguration());
        int res = FFmpegHandle.getInstance().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() {
                        StringBuilder sb = new StringBuilder();
                        sb.append("pts: ").append(pts).append("\n");
                        sb.append("dts: ").append(dts).append("\n");
                        sb.append("duration: ").append(duration).append("\n");
                        sb.append("index: ").append(index).append("\n");
                        tvPushInfo.setText(sb.toString());
                    }
                });
            }
        });
        log("result " + res);
    }

    public void btnPush(View view) {
        final String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "sample.flv";
        File file = new File(path);
        log(path + "  " + file.exists());
        new Thread() {
            @Override
            public void run() {
                super.run();
                int result = FFmpegHandle.getInstance().pushRtmpFile(path);
                log("result " + result);
            }
        }.start();
    }


    public void log(String content) {
        Log.w("eric", content);
    }
}

ffmpeg_handle.cpp

//
// Created by eric on 2017/11/1.
//
#include <jni.h>
#include <string>
#include<android/log.h>
#include <exception>

//定义日志宏变量
#define logw(content)   __android_log_write(ANDROID_LOG_WARN,"eric",content)
#define loge(content)   __android_log_write(ANDROID_LOG_ERROR,"eric",content)
#define logd(content)   __android_log_write(ANDROID_LOG_DEBUG,"eric",content)

extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
//引入时间
#include "libavutil/time.h"
}

#include <iostream>

using namespace std;

jobject pushCallback = NULL;
jclass cls = NULL;
jmethodID mid = NULL;

int callback(JNIEnv *env, int64_t pts, int64_t dts, int64_t duration, long long index) {
//    logw("=================")
    if (pushCallback == NULL) {
        return -3;
    }
    if (cls == NULL) {
        return -1;
    }
    if (mid == NULL) {
        return -2;
    }
    env->CallVoidMethod(pushCallback, mid, (jlong) pts, (jlong) dts, (jlong) duration,
                        (jlong) index);
    return 0;
}

int avError(int errNum) {
    char buf[1024];
    //获取错误信息
    av_strerror(errNum, buf, sizeof(buf));
    loge(string().append("发生异常:").append(buf).c_str());
    return -1;
}

//获取FFmpeg相关信息
extern "C"
JNIEXPORT jstring JNICALL
Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_getAvcodecConfiguration(JNIEnv *env,
                                                                        jobject instance) {
    char info[10000] = {0};
    sprintf(info, "%s\n", avcodec_configuration());
    return env->NewStringUTF(info);
}

/**
 * 设置回到对象
 */
extern "C"
JNIEXPORT jint JNICALL
Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_setCallback(JNIEnv *env, jobject instance,
                                                            jobject pushCallback1) {
    //转换为全局变量
    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);
    return 0;
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_pushRtmpFile(JNIEnv *env, jobject instance,
                                                             jstring path_) {
    const char *path = env->GetStringUTFChars(path_, 0);
    logw(path);
    int videoindex = -1;
    //所有代码执行之前要调用av_register_all和avformat_network_init
    //初始化所有的封装和解封装 flv mp4 mp3 mov。不包含编码和解码
    av_register_all();

    //初始化网络库
    avformat_network_init();

    const char *inUrl = path;
    //输出的地址
    const char *outUrl = "rtmp://192.168.31.127/live";

    //////////////////////////////////////////////////////////////////
    //                   输入流处理部分
    /////////////////////////////////////////////////////////////////
    //打开文件,解封装 avformat_open_input
    //AVFormatContext **ps  输入封装的上下文。包含所有的格式内容和所有的IO。如果是文件就是文件IO,网络就对应网络IO
    //const char *url  路径
    //AVInputFormt * fmt 封装器
    //AVDictionary ** options 参数设置
    AVFormatContext *ictx = NULL;

    AVFormatContext *octx = NULL;

    AVPacket pkt;
    int ret = 0;
    try {
        //打开文件,解封文件头
        ret = avformat_open_input(&ictx, inUrl, 0, NULL);
        if (ret < 0) {
            avError(ret);
            throw ret;
        }
        logd("avformat_open_input success!");
        //获取音频视频的信息 .h264 flv 没有头信息
        ret = avformat_find_stream_info(ictx, 0);
        if (ret != 0) {
            avError(ret);
            throw ret;
        }
        //打印视频视频信息
        //0打印所有  inUrl 打印时候显示,
        av_dump_format(ictx, 0, inUrl, 0);

        //////////////////////////////////////////////////////////////////
        //                   输出流处理部分
        /////////////////////////////////////////////////////////////////
        //如果是输入文件 flv可以不传,可以从文件中判断。如果是流则必须传
        //创建输出上下文
        ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
        if (ret < 0) {
            avError(ret);
            throw ret;
        }
        logd("avformat_alloc_output_context2 success!");

        int i;

        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;
        }

        //找到视频流的位置
        for (i = 0; i < ictx->nb_streams; i++) {
            if (ictx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
                videoindex = i;
                break;
            }
        }

        av_dump_format(octx, 0, outUrl, 1);
        //////////////////////////////////////////////////////////////////
        //                   准备推流
        /////////////////////////////////////////////////////////////////

        //打开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;
        }
        logd("avformat_write_header Success!");
        //推流每一帧数据
        //int64_t pts  [ pts*(num/den)  第几秒显示]
        //int64_t dts  解码时间 [P帧(相对于上一帧的变化) I帧(关键帧,完整的数据) B帧(上一帧和下一帧的变化)]  有了B帧压缩率更高。
        //获取当前的时间戳  微妙
        long long start_time = av_gettime();
        long long frame_index = 0;
        logd("start push >>>>>>>>>>>>>>>");
        while (1) {
            //输入输出视频流
            AVStream *in_stream, *out_stream;
            //获取解码前数据
            ret = av_read_frame(ictx, &pkt);
            if (ret < 0) {
                break;
            }

            /*
            PTS(Presentation Time Stamp)显示播放时间
            DTS(Decoding Time Stamp)解码时间
            */
            //没有显示时间(比如未解码的 H.264 )
            if (pkt.pts == AV_NOPTS_VALUE) {
                //AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。
                AVRational time_base1 = ictx->streams[videoindex]->time_base;

                //计算两帧之间的时间
                /*
                r_frame_rate 基流帧速率  (不是太懂)
                av_q2d 转化为double类型
                */
                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);
            }

            //延时
            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));
                }
            }

            in_stream = ictx->streams[pkt.stream_index];
            out_stream = octx->streams[pkt.stream_index];

            //计算延时后,重新指定时间戳
            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);
//        __android_log_print(ANDROID_LOG_WARN, "eric", "duration %d", pkt.duration);
            //字节流的位置,-1 表示不知道字节流位置
            pkt.pos = -1;

            if (pkt.stream_index == videoindex) {
                printf("Send %8d video frames to output URL\n", frame_index);
                frame_index++;
            }
            //回调数据
            callback(env, pkt.pts, pkt.dts, pkt.duration, frame_index);
            //向输出上下文发送(向地址推送)
            ret = av_interleaved_write_frame(octx, &pkt);

            if (ret < 0) {
                printf("发送数据包出错\n");
                break;
            }
            //释放
            av_packet_unref(&pkt);
        }
        ret = 0;
    } catch (int errNum) {
    }
    logd("finish===============");
    //关闭输出上下文,这个很关键。
    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);
    return ret;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,519评论 25 707
  • 简介 前面讲到了在Android平台下使用FFmpeg进行RTMP推流(视频文件推流),里面主要是介绍如何解析视频...
    第八区阅读 39,639评论 14 42
  • 登山何须杖,全凭脚来量。 会当凌绝顶,暮色暂昏黄。 权栖栈道阁,准时会太阳。 日着霓裳出,余景含羞藏。 杳湖山祁东...
    小启明星阅读 310评论 7 4
  • 心情确实会影响生活吧。我也不是故意这样。确实没必要这样的。但我就是这样做了。下次我会控制好情绪的。
    醉夏阅读 95评论 0 0
  • 女孩并没有回答,而是说“陪我去个地方吧!”“只要你愿意,天涯海角我也陪。”男孩坐上了自行车,女孩没说话,只是默默坐...
    淡幽阅读 209评论 0 0