本文主要了解通过FFmpeg在Android端来进行推流,其中推流的主要代码参考自雷神,我主要先了解其中一个大体的过程,里面的推流c代码没有去细究。本文要了解掌握的的知识点:
- FFmpeg在Android端推流一个视频文件
- 搭建简单的流媒体服务器(srs)
- 查看推出来的音视频数据流(使用VLC查看)
FFmpeg推流
本示例是和上一篇的整合一个FFmpeg so放在同一个module下面的,所以不用在引入FFmpeg文件了,直接专注和推流相关的事情。
编写推流底层C相关代码
在cpp文件下面新建一饿c文件streamer.c,这里是推流的关键代码:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <stdio.h>
#include <jni.h>
#include <time.h>
#include "include/libavcodec/avcodec.h"
#include "include/libavformat/avformat.h"
#include "include/libavutil/log.h"
#include "include/libavutil/time.h"
//Log
#ifdef ANDROID
#include <android/log.h>
#define LOGE(format, ...) __android_log_print(ANDROID_LOG_ERROR, "(>_<)", format, ##__VA_ARGS__)
#else
#define LOGE(format, ...) printf("(>_<) " format "\n", ##__VA_ARGS__)
#endif
/* Header for class com_lyman_ffmpeg_cmake_single_StreamerActivity */
#ifndef _Included_com_lyman_ffmpeg_cmake_single_StreamerActivity
#define _Included_com_lyman_ffmpeg_cmake_single_StreamerActivity
#ifdef __cplusplus
extern "C" {
#endif
void custom_log(void *ptr, int level, const char *fmt, va_list vl) {
//To TXT file
FILE *fp = fopen("/storage/emulated/0/av_log.txt", "a+");
if (fp) {
vfprintf(fp, fmt, vl);
fflush(fp);
fclose(fp);
}
//To Logcat
//LOGE(fmt, vl);
}
/*
* Class: com_lyman_ffmpeg_cmake_single_StreamerActivity
* Method: stream
* Signature: (Ljava/lang/String;Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_com_lyman_ffmpeg_1cmake_1single_StreamerActivity_stream
(JNIEnv *env, jobject obj, jstring input_jstr, jstring output_jstr) {
AVOutputFormat *ofmt = NULL;
AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
AVPacket pkt;
int ret, i;
char input_str[500] = {0};
char output_str[500] = {0};
char info[1000] = {0};
sprintf(input_str, "%s", (*env)->GetStringUTFChars(env, input_jstr, NULL));
sprintf(output_str, "%s", (*env)->GetStringUTFChars(env, output_jstr, NULL));
//input_str = "cuc_ieschool.flv";
//output_str = "rtmp://localhost/publishlive/livestream";
//output_str = "rtp://233.233.233.233:6666";
//FFmpeg av_log() callback
av_log_set_callback(custom_log);
av_register_all();
//Network
avformat_network_init();
//Input
if ((ret = avformat_open_input(&ifmt_ctx, input_str, 0, 0)) < 0) {
LOGE("Could not open input file.");
goto end;
}
if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
LOGE("Failed to retrieve input stream information");
goto end;
}
int videoindex = -1;
for (i = 0; i < ifmt_ctx->nb_streams; i++)
if (ifmt_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
videoindex = i;
break;
}
//Output
avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", output_str); //RTMP
//avformat_alloc_output_context2(&ofmt_ctx, NULL, "mpegts", output_str);//UDP
if (!ofmt_ctx) {
LOGE("Could not create output context\n");
ret = AVERROR_UNKNOWN;
goto end;
}
ofmt = ofmt_ctx->oformat;
for (i = 0; i < ifmt_ctx->nb_streams; i++) {
//Create output AVStream according to input AVStream
AVStream *in_stream = ifmt_ctx->streams[i];
AVStream *out_stream = avformat_new_stream(ofmt_ctx, in_stream->codec->codec);
if (!out_stream) {
LOGE("Failed allocating output stream\n");
ret = AVERROR_UNKNOWN;
goto end;
}
//Copy the settings of AVCodecContext
ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
if (ret < 0) {
LOGE("Failed to copy context from input to output stream codec context\n");
goto end;
}
out_stream->codec->codec_tag = 0;
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
}
//Open output URL
if (!(ofmt->flags & AVFMT_NOFILE)) {
ret = avio_open(&ofmt_ctx->pb, output_str, AVIO_FLAG_WRITE);
if (ret < 0) {
LOGE("Could not open output URL '%s'", output_str);
goto end;
}
}
//Write file header
ret = avformat_write_header(ofmt_ctx, NULL);
if (ret < 0) {
LOGE("Error occurred when opening output URL\n");
goto end;
}
int frame_index = 0;
int64_t start_time = av_gettime();
while (1) {
AVStream *in_stream, *out_stream;
//Get an AVPacket
ret = av_read_frame(ifmt_ctx, &pkt);
if (ret < 0)
break;
//FIX:No PTS (Example: Raw H.264)
//Simple Write PTS
if (pkt.pts == AV_NOPTS_VALUE) {
//Write PTS
AVRational time_base1 = ifmt_ctx->streams[videoindex]->time_base;
//Duration between 2 frames (us)
int64_t calc_duration =
(double) AV_TIME_BASE / av_q2d(ifmt_ctx->streams[videoindex]->r_frame_rate);
//Parameters
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);
}
//Important:Delay
if (pkt.stream_index == videoindex) {
AVRational time_base = ifmt_ctx->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;
if (pts_time > now_time)
av_usleep(pts_time - now_time);
}
in_stream = ifmt_ctx->streams[pkt.stream_index];
out_stream = ofmt_ctx->streams[pkt.stream_index];
/* copy packet */
//Convert PTS/DTS
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base,
AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base,
AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos = -1;
//Print to Screen
if (pkt.stream_index == videoindex) {
LOGE("Send %8d video frames to output URL\n", frame_index);
frame_index++;
}
//ret = av_write_frame(ofmt_ctx, &pkt);
ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
if (ret < 0) {
LOGE("Error muxing packet\n");
break;
}
av_free_packet(&pkt);
}
//Write file trailer
av_write_trailer(ofmt_ctx);
end:
avformat_close_input(&ifmt_ctx);
/* close output */
if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
avio_close(ofmt_ctx->pb);
avformat_free_context(ofmt_ctx);
if (ret < 0 && ret != AVERROR_EOF) {
LOGE("Error occurred.\n");
return -1;
}
return 0;
}
#ifdef __cplusplus
}
#endif
#endif
配置CmakeLists.txt
这里相当于要添加一个streamer-lib.c生成的so,注意这个so依赖ffmpeg so的配置,相关的配置如下:
add_library( # Sets the name of the library.
streamer-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/streamer-lib.c)
target_link_libraries(
streamer-lib
libffmpeg
${log-lib})
Java层调用Native方法
我重新新建了一个StreamerActivity来调用实现,其中有一个第一次拷贝res下面的一个视频文件到手机存储目录上面,便于我们调用推流的时候路径的写入:
package com.lyman.ffmpeg_cmake_single;
import android.content.Context;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.Toast;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class StreamerActivity extends AppCompatActivity {
private static final String TAG = "StreamerActivity";
private ProgressBar mProgressBar;
private static String mPushStreamPath = "rtmp://192.168.0.101/live/livestream";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_streamer);
if (!getMoveFile().exists()) {
new Thread(new Runnable() {
@Override
public void run() {
copyFilesFromRaw(StreamerActivity.this, R.raw.i_am_you,
"i_am_you.mp4",
getExternalFilesDir(Environment.DIRECTORY_MOVIES).getAbsolutePath());
}
}).start();
}
mProgressBar = findViewById(R.id.progressBar);
}
public void onClickPushStream(View View) {
if (!getMoveFile().exists()) {
Toast.makeText(this, "source file not exist", Toast.LENGTH_SHORT).show();
return;
}
mProgressBar.setVisibility(View.VISIBLE);
new Thread(new Runnable() {
@Override
public void run() {
stream(getMoveFile().getAbsolutePath(), mPushStreamPath);
runOnUiThread(new Runnable() {
@Override
public void run() {
mProgressBar.setVisibility(0x00000008);
}
});
}
}).start();
}
public native int stream(String inputurl, String outputurl);
static {
System.loadLibrary("streamer-lib");
}
private File getMoveFile() {
File rootFile = getExternalFilesDir(Environment.DIRECTORY_MOVIES);
// Create the storage directory if it does not exist
if (!rootFile.exists()) {
if (!rootFile.mkdirs()) {
Log.d(TAG, "failed to create directory");
return null;
}
}
String fileName = "i_am_you.mp4";
String path = rootFile.getAbsolutePath() + File.separator + fileName;
return new File(path);
}
private void copyFilesFromRaw(Context context, int id, String fileName, String storagePath) {
InputStream inputStream = context.getResources().openRawResource(id);
File file = new File(storagePath);
if (!file.exists()) {//如果文件夹不存在,则创建新的文件夹
file.mkdirs();
}
readInputStream(storagePath + File.separator + fileName, inputStream);
}
/**
* 读取输入流中的数据写入输出流
*
* @param storagePath 目标文件路径
* @param inputStream 输入流
*/
private void readInputStream(String storagePath, InputStream inputStream) {
File file = new File(storagePath);
try {
if (!file.exists()) {
// 1.建立通道对象
FileOutputStream fos = new FileOutputStream(file);
// 2.定义存储空间
byte[] buffer = new byte[inputStream.available()];
// 3.开始读文件
int lenght = 0;
while ((lenght = inputStream.read(buffer)) != -1) {// 循环从输入流读取buffer字节
// 将Buffer中的数据写到outputStream对象中
fos.write(buffer, 0, lenght);
}
fos.flush();// 刷新缓冲区
// 4.关闭流
fos.close();
inputStream.close();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
OK,现在build我们的项目,如果没有问题就可以在jniLibs下面看到生成的libstreamer-lib.so文件了。
现在运行项目肯定回看到这个错误:
很明显打不开那个RTMP的地址,这个地址我们到下面在仔细看。我们现在要在电脑上面搭建一个流媒体服务器,这个服务器的种类有很多,例如RED5,srs等,我下面选择的是srs
搭建SRS的RTMP服务器
我使用的系统是Mac(10.13.3),构建通过了。
下载编译SRS
下载SRS源码
srs的github地址为https://github.com/ossrs/srs,但是现在他们已经不在维护更新了,我下面使用的srs的源码是官方推荐的一个fork:https://github.com/smartdu/srs。第一个官网的代码我编译下来会有错误,于是没有用那个了
编译SRS源码
首先看看我们下载的SRS的项目包结构:
cd到trunk目录下面
指向如下命令:
./configure --osx --without-ssl --without-hls --without-hds --without-dvr --without-nginx --without-http-callback --without-http-server --without-stream-caster --without-http-api --without-ffmpeg --without-transcode --without-ingest --without-stat --without-librtmp
make
编译成功以后会生成srs/trunk/objs/srs这个可执行文件
运行SRS服务器
看到别人的博文写的Mac系统下在运行之前要修改一下srs/trunk/conf/src.conf文件里面的并发连接的个数。
max_connections 100;
开启运行srs服务器命令
cd到trunk目录下面:
./etc/init.d/srs start
./etc/init.d/srs stop
./etc/init.d/srs restart
上面的三个指令的意思想必一看就懂。服务器起来以后在终端会看到:
就代表服务器成功起来了
另外还有几个命令可以帮助我们看一些信息:
#跑起来后要留意下log,配置存在错误的话,srs进程会关闭掉
tail -f ./objs/srs.log
#检查下srs进程是否正常运行
ps aux | grep srs
推流和播放视频
上面我们已经把服务器成功起来了就可以进行这里的工作了,在上面我运行代码有一个错误,不能打开。。。。那其中有一个rtmp的地址信息,这个要稍微注意一下,那个地址是我的电脑的地址:
我的rtmp地址为:
rtmp://192.168.0.101/live/livestream
后面加了live/livestream这个具体的意义我没有去看,可以随便命名。把这个地址配置到我们项目的代码的推流地址上。
在VLC上面播放上面的rtmp流地址:
-
-
- 当我们把srs服务器开着的时候,那么VLC就会到一个等待的界面:
这时我们在手机应用上面点击开始推流看看效果:
终于看到了效果QAQ。
但是有一个问题,我的视频文件是四分多钟的,但是我推出去在电脑上面看到的很短,并且AS打印推出的视频也只有几百帧。我想这是代码处理上面有问题,现在不深究了。
项目完整代码:查看