// 본글은 여러 블로그를 참고하여 작성되었습니다. //
// 작성 일자 : 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() { | |
} | |
} |
이제 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; | |
} |
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) |
다만, 여기에서 주의할 점은 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를 사용하면 끝
'Android Story' 카테고리의 다른 글
[Android ] 안드로이드 UI 디자인, Framer (1) | 2017.12.22 |
---|---|
[ Android ] JavaCV 연동 (0) | 2017.03.30 |
[ Android ] 틴트(오버레이) 색상 지정하기 (0) | 2017.03.28 |
[ Android ] 줄번호 설정 (0) | 2017.01.13 |
[ Android ] Network type (0) | 2016.12.19 |
[ Android ] FFplay 문서 번역 (0) | 2016.12.13 |
[ Android ] FFMpeg 패키지 연동하기 (15) | 2016.12.13 |
[ Android ] inflate (0) | 2016.12.07 |