Android Story

[ Android ] FFmpeg Rtsp Player 사용하기

WhiteDuck 2016. 12. 27. 15:09


FFmpeg에 대한 세팅은 


[Android] FFmpeg 패캐지 연동하기


에서 살펴보았다.


// 본글은 여러 블로그를 참고하여 작성되었습니다. //

// 작성 일자 : 2016.12.27 //


이번 포스팅에서는 FFmpeg은 jni에서 java에서는 Surface를 사용하여 Rtsp Player를 만들어보고자 한다.


다만 VideoStream에 국한적인 플레이어이니 참고바란다.


이후에 AudioStream 연동에 대해서 해보고 난 뒤 포스팅할 예정이다.


 

참고 : 

    Android+FFmpeg+ANativeWindow视频解码播放


사실 참고한 곳에 더 잘 나와있다.


저번시간에 FFmpeg 3.x 버전으로 세팅하였었는데, 자사의 개발 서버와 FFmpeg 을 맞추는 것이 좋을 것 같아,


FFmpeg 2.8.10 버전으로 세팅하였음을 알린다.



test rtsp uri :  "rtsp://184.72.239.149/vod/mp4:BigBuckBunny_115k.mov"




우선 파일 트리는 다음과 같다.




Activity에서는 RtspPlayView를 생성하는 데 소스에는 uri를 생성자 파라미터에 포함하였다. 


이왕이면 setDataSource() 같은 메소드로 따로 분류하여 적용하는 것이 깔끔해 보일 것이다.


public class MainActivity extends AppCompatActivity{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
RtspPlayView playView = new RtspPlayView(getApplicationContext(), "rtsp://184.72.239.149/vod/mp4:BigBuckBunny_115k.mov");
setContentView(playView);
}
}





 


RtspPlayer에서는 NDKAdapter를 통해서 JNI를 참조하고, MainActivity의 SurfaceView에 전달한다.


import android.content.Context;
import android.graphics.Canvas;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
/**
* Rtsp player
* Created by user on 12/22/16.
*/
public class RtspPlayView extends SurfaceView implements SurfaceHolder.Callback{
private static final String TAG = "RtspPlayView";
private SurfaceHolder mHolder;
private NDKAdapter mPlayerNdkAdapter;
public RtspPlayView(Context context, String uri) {
super(context);
mHolder = getHolder();
mHolder.addCallback(this);
// JNI에 있는 라이브러리를 통해서 작업한다.
mPlayerNdkAdapter = new NDKAdapter();
mPlayerNdkAdapter.setDataSource(uri);
}
/** -----------------------------
* SurfaceHolder.Callback Implementation
*/
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
// 비디오 플레이는 시간이 많이 걸리는 작업이여서 작업도중 메인 UI 쓰레드를 차단하지 않기 위해서, 별도의 쓰레드를 통해서 재생한다.
new Thread(new Runnable() {
@Override
public void run() {
mPlayerNdkAdapter.play(mHolder.getSurface());
}
}).start();
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
}
}




NDKAdapter는 JNI 라이브러리를 불어오는 역할만 수행한다.

/**
* Created by user on 12/12/16.
*/
public class NDKAdapter {
static {
System.loadLibrary("VideoPlayer");
}
public static native void setDataSource(String uri);
public static native int play(Object surface);
public NDKAdapter() {
}
}
view raw NDKAdapter.java hosted with ❤ by GitHub




이제 MakeProject 뒤에 NDKAdapter.java 를 javah 하면 jni에서 사용할 .h 파일이 생긴다. ( 모르시는 분은 [Android] FFmpeg 패캐지 연동하기 을 참고하세요)


이것을 적용할 interface.c 파일을 만들고 다음과 같이 기입한다.


#include "../com_gluesys_util_NDKAdapter.h"
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <android/log.h>
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, "libnav", __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG , "libnav", __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO , "libnav", __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN , "libnav", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , "libnav", __VA_ARGS__)
char *uri;
JNIEXPORT void JNICALL Java_com_gluesys_util_NDKAdapter_setDataSource(JNIEnv *env, jclass clazz, jstring _uri){
uri = (*env)->GetStringUTFChars(env, _uri, NULL);
}
JNIEXPORT jint JNICALL Java_com_gluesys_util_NDKAdapter_play(JNIEnv * env, jclass clazz, jobject surface)
{
LOGD("play");
// it must set to setDataSource for play.
const char * file_name = uri ;
if( file_name == NULL ) {
LOGE("Please set the DataSource");
return -1;
}
av_register_all(); // get muxer, demuxer and protocol definitions.
AVFormatContext * pFormatCtx = avformat_alloc_context();
// Open video file
if(avformat_open_input(&pFormatCtx, file_name, NULL, NULL)!=0) {
LOGE("Couldn't open file:%s\n", file_name);
return -1; // Couldn't open file
}
// Retrieve stream information
if(avformat_find_stream_info(pFormatCtx, NULL)<0) {
LOGE("Couldn't find stream information.");
return -1;
}
// Find the first video stream
int videoStream = -1, i;
for (i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO
&& videoStream < 0) {
videoStream = i;
}
}
if(videoStream==-1) {
LOGE("Didn't find a video stream.");
return -1; // Didn't find a video stream
}
// Get a pointer to the codec context for the video stream
AVCodecContext * pCodecCtx = pFormatCtx->streams[videoStream]->codec;
// Find the decoder for the video stream
AVCodec * pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
LOGE("Codec not found.");
return -1; // Codec not found
}
if(avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
LOGE("Could not open codec.");
return -1; // Could not open codec
}
// get native window that called by surface in java.
ANativeWindow* nativeWindow = ANativeWindow_fromSurface(env, surface);
// get codec size
int videoWidth = pCodecCtx->width;
int videoHeight = pCodecCtx->height;
// buffer size is decided native window size
ANativeWindow_setBuffersGeometry(nativeWindow, videoWidth, videoHeight, WINDOW_FORMAT_RGBA_8888);
ANativeWindow_Buffer windowBuffer;
if(avcodec_open2(pCodecCtx, pCodec, NULL)<0) {
LOGE("Could not open codec.");
return -1; // Could not open codec
}
// Allocate video frame
AVFrame * pFrame = av_frame_alloc();
// rendering
AVFrame * pFrameRGBA = av_frame_alloc();
if(pFrameRGBA == NULL || pFrame == NULL) {
LOGE("Could not allocate video frame.");
return -1;
}
// Determine required buffer size and allocate buffer
int numBytes=av_image_get_buffer_size(AV_PIX_FMT_RGBA, pCodecCtx->width, pCodecCtx->height, 1);
uint8_t * buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
av_image_fill_arrays(pFrameRGBA->data, pFrameRGBA->linesize, buffer, AV_PIX_FMT_RGBA,
pCodecCtx->width, pCodecCtx->height, 1);
// set stream-data-format from YUV to RGBA.
struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width,
pCodecCtx->height,
pCodecCtx->pix_fmt,
pCodecCtx->width,
pCodecCtx->height,
AV_PIX_FMT_RGBA,
SWS_BILINEAR,
NULL,
NULL,
NULL);
int frameFinished;
AVPacket packet;
while(av_read_frame(pFormatCtx, &packet)>=0) {
// Is this a packet from the video stream?
if(packet.stream_index==videoStream) {
// Decode video frame
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
if (frameFinished) {
// lock native window buffer
ANativeWindow_lock(nativeWindow, &windowBuffer, 0);
// convert format
sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
pFrame->linesize, 0, pCodecCtx->height,
pFrameRGBA->data, pFrameRGBA->linesize);
// calculate stride.
uint8_t * dst = windowBuffer.bits;
int dstStride = windowBuffer.stride * 4;
uint8_t * src = (uint8_t*) (pFrameRGBA->data[0]);
int srcStride = pFrameRGBA->linesize[0];
// Depending on the stride and gait window frame, thus requiring a progressive copy
int h;
for (h = 0; h < videoHeight; h++) {
memcpy(dst + h * dstStride, src + h * srcStride, srcStride);
}
ANativeWindow_unlockAndPost(nativeWindow);
}
}
av_packet_unref(&packet);
}
av_free(buffer);
av_free(pFrameRGBA);
// Free the YUV frame
av_free(pFrame);
// Close the codecs
avcodec_close(pCodecCtx);
// Close the video file
avformat_close_input(&pFormatCtx);
return 0;
}
view raw interface.c hosted with ❤ by GitHub




Android.mk 파일은 VideoPlayer 라이브러리를 생성하도록 하고, interface.c를 사용한다.


LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := interface.c
LOCAL_LDLIBS += -llog -lz -landroid
LOCAL_MODULE := VideoPlayer
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
LOCAL_SHARED_LIBRARIES:= avcodec avformat avutil swresample swscale
include $(BUILD_SHARED_LIBRARY)
$(call import-module,ffmpeg-2.8.10/android/arm)
view raw Android.mk hosted with ❤ by GitHub



다만, 여기에서 주의할 점은 Surface를 jni에서 사용하기 위해서 Native_window를 사용하였다는 것이다.


이 것을 얻어오기 위해서 


jni

└ include

        └  android

                  └ native_window.h

                  └ native_window_jni.h


 

와 같이 배치한다. 각 파일은 NDK 경로의 platforms 하위에서 검색하면 나온다. 


여러개나 나오지만 Android-version이 달라 여러개가 나올뿐 소스는 같다. ( 몇개만 확인해 봤을때 )


저자는 /home/user/Android/Sdk/ndk-bundle/platforms 에서 native_window를 검색하였다.


이제 jni를 ndk-build를 사용하면 끝




반응형