주요글: 도커 시작하기

자바에서 Ffmpeg을 사용하려고 Xuggler 5.4 버전을 사용했는데, 이게 64비트 윈도우에서 종종 JVM이 Crash되는 문제가 있음을 발견했다. (다른 OS에서는 안 해봐서 어떤지 모른다.) Xuggler 5.5 정식 버전이 나오기까지 기다리긴 힘들기도 해서 자바 코드에서 ffmpeg을 직접 호출해야 하나라는 생각을 하다가, JAVE라는 ffmpeg 래퍼를 찾게 되어서 소개한다.


JAVE(Java Audio Video Encoder)


JAVE는 Runtime.exec()를 이용해서 ffmpeg을 실행해주는 래퍼이다. JAVE는 ffmpeg이 출력하는 문자열을 분석해서 알맞은 정보를 제공해준다. 예를 들어,아래 코드를 보자.


Encoder encoder = new Encoder();

MultimediaInfo info = encoder.getInfo(new File("src/test/resources/sample.mp4"));

System.out.println(info.getFormat());

System.out.println(info.getAudio().getDecoder());

System.out.println(info.getVideo().getDecoder());


위 코드에서 encoder.getInfo() 메서드를 실행하면 ffmpeg -i src/test/resources/sample.mp4 명령어가 실행되고, 출력된 문자열을 파싱해서 컨테이너 포맷이나 비디오/오디오 코덱에 대한 정보를 생성해준다.


트랜스코딩은 다음의 코드를 사용해서 실행한다.


File source = new File("source.avi");

File target = new File("target.flv");


AudioAttributes audio = new AudioAttributes();

audio.setCodec("libmp3lame");

audio.setBitRate(new Integer(64000));

audio.setChannels(new Integer(1));

audio.setSamplingRate(new Integer(22050));


VideoAttributes video = new VideoAttributes();

video.setCodec("flv");

video.setBitRate(new Integer(160000));

video.setFrameRate(new Integer(15));

video.setSize(new VideoSize(400, 300));


EncodingAttributes attrs = new EncodingAttributes();

attrs.setFormat("flv");

attrs.setAudioAttributes(audio);

attrs.setVideoAttributes(video);


Encoder encoder = new Encoder();

encoder.encode(source, target, attrs);


JAVE의 자세한 사용 방법은 http://www.sauronsoftware.it/projects/jave/manual.php 에 있으니 참고하기 바란다.


JAVE의 재미난 점


JAVE의 Encoder가 ffmpeg 실행 파일의 경로를 찾을 때 사용하는 FFMPEGLocator를 사용하는데, 별도로 FFMPEGLocator를 지정하지 않으면 DefaultFFMPEGLocator이 사용된다. DefaultFFMPEGLocator는 다음의 작업을 수행한다.

  • java.tmp.dir 시스템 속성을 이용해서 임시 디렉토리를 구한다.
  • 임시 디렉토리에 jar 파일에 포함된 ffmpeg 관련 파일을 복사한다.
    • 윈도우인 경우 jar 파일에 포함된 ffmpeg.exe와 pthreadGC2.dll 파일일 임시 디렉토리에 복사한다.
    • 리눅스인 경우 jar 파일에 포함된 ffmpeg 파일을 임시 디렉토리에 복사한다.
  • ffmpeg 실행 파일의 경로로 임시 디렉토리에 복사한 ffmpeg.exe 또는 ffmpeg 파일의 경로를 제공한다.
jar 파일에 포함되어 있는 ffmpeg 실행 파일을 사용하기 때문에, ffmpeg이 로컬 시스템에 설치되어 있지 않아도 정상적으로 실행할 수 있다.

아쉬운 점은 썸네일 생성 기능을 래핑하지 않았다는 점인데, 이것까지 들어가면 더 좋을 것 같다.


관런 링크

  • 프로젝트 사이트: http://www.sauronsoftware.it/projects/jave/


  1. 질문자 2017.03.31 10:16

    자바 7버젼에서는 사용 불가능한가요?
    8버젼에서 잘 돌아갔는데 JAVA 1.7에서는

    Exception in thread "main" it.sauronsoftware.jave.InputFormatException
    at it.sauronsoftware.jave.Encoder.parseMultimediaInfo(Encoder.java:659)
    at it.sauronsoftware.jave.Encoder.getInfo(Encoder.java:487)
    at ColorDetection.MediaInformation(ColorDetection.java:45)
    at ColorDetection.main(ColorDetection.java:24)

    이런 에러가 발생합니다 ㅠㅠ

동영상 스트리밍을 위해 최초에 선택한 방법은 다음과 같았다.

  • PC 웹 브라우저: RTMP 프로토콜 이용 스트리밍/플래시 플레이어 이용.
  • 안드로이드: RTSP 프로토콜 이용 스트리밍, 안드로이드 MediaPlayer 이용.

미디어 서버로는 Wowza를 선택했는데, 그 이유는 위 두 가지 프로토콜을 모두 지원하기 때문이었다. 


안드로이드와 RTSP의 나쁜 궁합


그런데, 기능 구현을 진행하다보니 안드로이드에서 다음의 문제점들이 드러났다.

  • 스트리밍 품질
  • seeking 기능의 문제
우선, 스트리밍 자체의 품질이 좋지 않았다. RTSP 자체가 데이터 송수신에 UDP를 사용하는 것에서 비롯되는 것도 있겠지만, 안드로이드 2.2 기반 폰, 3.2 기반 태블릿, 4.0 기반 폰, 4.1 기반 넥서스 7에서 화면이 일부 깨지거나 하는 등의 현상이 발생했다.

특히 문제되는 부분은 시간바의 이동, 즉 seeking 기능에 있었다. seek bar를 이동하는 동안 플레이어가 해당 시점으로 이동하고 버퍼링을 하는 등의 작업을 하는데, RTSP의 경우 시간 이동과 버퍼링 등이 신속하게/원활하게 동작하지 않는 경우가 많았다. 특히, 허니콤 기반의 갤럭시 탭은 시간 이동이 안 될 정도로 문제가 심각했다. (갤럭시 탭을 아이스크림으로 업그레이드 해야 그나마 seek bar 이동이 동작했다.)

안드로이드의 MediaPlayer와 RTSP와의 궁합이 그닥 좋지 않다는 점, 특히 RTSP를 사용할 때 seeking이 부드럽게 되지 않는다는 점 때문에 스트리밍 방식을 교체하기로 결정했다.

HTTP 기반 스트리밍으로의 전환

여러 방법을 고민하다가 HTTP 기반의 스트리밍으로 처리하기로 결정했다. 필요한 건 다음과 같은 것들이다.
  • HTTP range 헤더를 지원하는 웹 서버. 아파치 httpd는 당연히 지원하므로, 아파치 웹 서버를 사용 (seeking과 관련)
  • 힌팅(hinting)된 MP4 파일 (비디오는 H.264, 오디오는 AAC로 인코딩 된 버전)
안드로이드의 MediaPlayer는 힌팅된 MP4 파일을 HTTP 기반으로 스트리밍으로 플레이 할 수 있는 기능을 제공하고 있다. 따라서, 아파치 웹 서버의 문서 디렉토리에 MP4 파일을 업로드 하고, MediaPlayer의 데이터 소스로 다음과 같이 MP4 파일에 대한 URL을 지정하면 해당 MP4를 플레이할 수 있게 된다.

path = "http://mediaserver/starwords_1.mp4";
mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(path);
...

MediaPlayer의 Seeking 기능을 사용할 경우 HTTP의 range 헤더를 이용해서 원하는 위치로 빠르게 이동할 수 있게 된다. 즉, 프로그레시브 다운로드Progressive Download 방식의 비디오 플레이와 달리 원하는 위치로 빠르게 이동할 수 있게 된다.

안드로이드 앱에서 동영상 미디어를 스트리밍으로 플레이하는 기능이 필요하다면, 현재 수준에서는 폼질/안정성 측면을 고려한다면 여러모로 불안한 RTSP 보다는 HTTP 기반 스트리밍을 사용하는 것이 현명할 것이다.

참고:


  1. 2012.10.31 21:04

    비밀댓글입니다

Flowplayer를 이용한 이어보기 기능을 구현해 보았다. 몇 번의 삽질 끝에 잘 동작하는 버전을 정리한다. 관련 코드는 아래와 같다.


<script type='text/javascript'>

var seekTime = 0; // 이어보기 할 시간을 저장

function initPlayer() {

flowplayer("player", "/player/flowplayer-3.2.12.swf", {

log: { level: 'debug', filter: 'org.flowplayer.captions.*' },

clip: {

url: 'RTMP동영상URL', 

provider: 'rtmp',

autoPlay: false, // 자동 시작 아님

onStart: function() {

if (seekTime > 0) {  // 이어보기 시간이 0보다 크면

$f("player").seek(seekTime); // seek 이용 시간 이동

}

}

},

plugins:  {

rtmp: {

url: "/player/flowplayer.rtmp-3.2.10.swf",

netConnectionUrl: 'rtmp://wowza.racon.scgs.co.kr:1935/vod'

},

controls: {

url: "flowplayer.controls-3.2.12.swf"

}

}

});

setTimeout(function() {

seek();

},1000);

}

function seek() {

if (!aleadyViewed) { // aleadyViewed는 처음 보는 영상인지의 여부를 보관

$f("player").play(); // 첨보는 영상이면 바로 플레이

return;

}


if(confirm("이어보시겠습니까?")) { // 이미 시청했다면

seekTime = ${elapsedTime}; // 시청한 시간을 seekTime으로 지정

}

$f("player").play();

}

</script>


전에 시청한 시간은 다음의 두 가지 방법으로 기록하면 된다.

  • 플레이 도중 다른 페이지로 이동할 때, 플레이 시간을 기록
  • 또는 10초 내지 20초 단위로 주기적으로 플레이 시간을 기록
플레이 시간은 getTime() API를 이용해서 구할 수 있다.

var time = $f("player").getTime(); // 초단위로 값 구함


  1. 2013.10.15 13:23

    비밀댓글입니다

사이트에 동영상 플레이 기능을 넣고 싶을 때 고려해볼만한 플레이어가 Flowplayer이다. HTML5의 Video 관련 기능이 아직 완전하게 표준화되지 않은 시점이기에, PC 기반의 웹 브라우저에서 동영상을 플레이하는 가장 현실적인 방안은 플래시를 사용하는 것이다. 하지만, 규모가 작은 회사에서 플래시 개발자를 고용하거나 플래시 기반의 동영상 플레이어를 외주 주는 것은 비용 문제 때문에 쉽지가 않다. 이럴 때 바로 유용하게 사용할 수 있는 것이 Flowplayer (http://flowplayer.org/)이다.


Flowplayer는 GPL 3 라이선스를 따르는 오픈 소스로 제공되는 플레이어로서 라이선스를 따르는 한 자유롭게 사용할 수 있다. 또한, 로고를 마음대로 변경할 수 있는 상용 버전도 1개 도메인에 대해 95$ 밖에 안 하기 때문에 매우 싼 값에 플래시 기반의 동영상 플레이어를 사용할 수 있다. 게다가 다양한 플러그인들이 존재해서 필요한 기능들이 이미 존재하는 경우가 많았다. 이런 이유로 필자도 FlowPlayer를 선택하게 되었다.


Flowplayer의 주요 기능


동영상 플레이어니까 당연히 동영상을 재생해주는 기능을 제공하는데, 간략하게 주요 기능들을 정리해보면 다음과 같다.

  • 손쉬운 설치와 설정
  • pseudostreaming 지원 / RTMP 지원 /  YouTube 비디오 재생
  • 플레이어 이벤트를 처리할 수 있는 다양한 자바스크립트 API
  • 플러그인을 통한 손쉬운 기능 확장
  • ABR(Adaptive Bitrate) 지원
  • URL Obfuscation 기능
Flowplayer 자체는 매우 설치하기 쉽다. 다운로드 받은 파일을 웹 서버의 알맞은 곳에 올리고 간단한 자바 스크립트 코드를 이용하면 된다. 아래 코드는 RTMP 기반의 플레이 설정 예이다.


<script src="jquery-1.7.1.js"></script>

<script src="flowplayer-3.2.11.min.js"></script>


<script type="text/javascript">

$(function() {

flowplayer("player", "flowplayer-3.2.12.swf", {

clip: {

url: 'mp4:0000/00/1/1_PC1.mp4',

provider: 'rtmp'

},

plugins:  {

rtmp: {

url: "flowplayer.rtmp-3.2.10.swf",

netConnectionUrl: 'rtmp://wowza.myserver-host.com:1935/vod'

}

}

});

});

</script>

</head>

<body>

<div id="player" style="width: 644px; height: 276px; margin: 0 auto; text-align: center">

</div>

</body>

</html>


모든 설정이 위와 같이 JSON 형식의 자바 스크립트 코드로 이루어지기 때문에, Flowplayer 사이트만 참고하면 비교적 쉽게 동영상 재생 기능을 사이트에 추가할 수 있다.


다양한 이벤트 지원


동영상 재생이 종료되면 사용자에게 추가 정보를 알려주는 레이어를 띄워 주고 싶을 수 있다. 예를 들어, 상품에 대한 리뷰 동영상의 재생이 끝나면 상품에 대한 상세 보기 페이지로 이동할 수 있는 안내 레이어를 보여주고 싶은 경우가 있을 것이다. 이렇게 플레이어의 상태에 따라 특정한 동작을 하고 싶은 경우에는 Flowplayer가 제공하는 이벤트를 사용하면 된다.


플레이어가 제공하는 이벤트는 다음과 같은 것들이 있다.

  • volume: 볼륨 조절시 발생
  • load: 플레이어가 완전히 로딩될 때 발생
  • fullscreen, fullscreenExit: 풀스크린 모드 전환이나 취소시 발생
  • mouseOver, mouseOut: 플레이어 위에 마우스가 올라가거나 벗어날 때 발생
개별 클립에 대한 이벤트로는 다음과 같은 것들이 있다.
  • begin: 클립이 시작될 때 발생
  • finish: 클립이 끝날 때 발생
  • pause: 일시 중지할 때 발생
  • stop: 멈출 때 발생
  • seek: 시간을 이동했을 때 발생
  • cuepoint: 지정한 시간에 발생

위 이벤트에 반응하는 코드는 자바 스크립트를 이용해서 작성할 수 있다. 따라서, 자바 스크립트 코드만 작성할 수 있다면 각 이벤트가 발생할 때 원하는 기능을 수행할 수 있다.


유용한 플러그인들


Flowplayer의 장점 중 하나는 플러그인에 있다. 플래시와 자바 스크립트의 두 종료 플러그인이 존재하는데, 몇 가지 유용한 플러그인을 소개하도록 하겠다.


Content 플러그인


Content 플러그인은 플레이어 화면 위에 원하는 HTML을 보여줄 수 있는 기능이다. 링크 등을 걸 수 있기 때문에, 시청 도중에 관련 상품을 보여준다거나 종료 후 추가 정보를 보여주고 싶을 때 유용하게 사용할 수 있다.


[발췌: Flowpalyer]


Caption 플러그인


Caption 플러그인을 사용하면 자막을 올릴 수 있다. 아래는 Caption 플러그인을 사용해서 자막을 넣은 예를 보여주고 있다.


[발췌: Flowpalyer]


Menu 플러그인


Menu 플러그인을 사용하면 컨트롤바에 메뉴를 위한 버튼이 추가된다. 해당 버튼을 클릭하면 메뉴가 화면에 표시된다. 메뉴 목록은 자바 스크립트 코드를 이용해서 입력할 수 있다. 관련 비디오 목록을 보여주는 용도로 사용할 수 있을 것이다.


[발췌: Flowpalyer]


오픈소스와 플러그인 확장


개발자 입장에서 Flowplayer의 가장 매력적인 점은 오픈 소스이면서 플러그인을 쉽게 확장할 수 있다는 점이다. 컨트롤바부터 모든 것이 플러그인을 처리되기 때문에 플래시 개발 역량을 조금만 키우면 Flowplayer의 플레이 기능을 사용하면서 동시에 원하는 기능을 제공하는 플러그인을 추가할 수 있다.

  1. 권남 2012.07.06 19:31

    저희는 FlowPlayer를 사용하다가 HTML5 video + video.js 기반으로 변경하였습니다.
    Flash를 사용하기 보다는 HTML5와 그 fallback(video.js 나 jwPlayer 등)을 저는 권하고 싶습니다.
    MP4 코덱 + HTML5 Video + video.js 로 작업하면 스마트폰, 태블릿, PC 모두 아울러서 거의 모든 현대적인 브라우저에서 재생가능합니다.

    단, 플러그인 등은 당연히 부족합니다.

    • 최범균 madvirus 2012.07.06 20:16 신고

      아직 HTML5로는 RTMP와 같은 스트리밍을 할 수 없어서 Flowplayer를 선택했어요. 아무래도 동영상 스트리밍을 하기에 video 태그는 무리네요.

미디어서버를 이용해서 동영상 서비스를 제공하고 싶을 때 문제가 되는 부분은 동영상 주소가 웹 소스 상에 그대로 노출된다는 점이다. 예를 들어, 아래 코드를 보자.


<a href="http://somehost/vod/_definst_/mp4:0000/00/1/1_PC1.mp4/playlist.m3u8">


조금만 지식이 있다면 위 코드를 이용해서 아무 곳에서나 동영상을 플레이할 수 있게 된다. 자유롭게 사용할 수 있도록 해 주는 것이 문제가 되지 않는다면 상관없지만, 만약 동영상 컨텐츠를 허용된 방법 이외의 다른 방법으로 재생하거나 동영상 자체를 다운로드 할 수 없도록 하고 싶다면 별도의 방안을 강구해야 한다.


DRM을 사용할 수 있겠지만 DRM 솔루션 자체가 비싸고 플레이어를 별도로 구현해야 한다는 단점이 있다. 또한, 디바이스에 따라 플레이어를 커스터마이징 할 수 없는 경우가 발생할 수도 있다. 그래서 생각해 볼 수 있는 것이 아래와 같이 동영상 주소를 시간에 따라서 다르게 생성해주는 것이다.


<a href="http://somehost/vod/_definst_/E14c1d156a...무지긴문자열...72ea34e4/playlist.m3u8">


위 값을 생성하는 방식에 따라 원하는 기능을 구현할 수 있다. 예를 들어, 암호화할 때 현재 시간 값을 포함시켜서 일정 시간이 지나면 값이 무효화되도록 구현할 수 있다. 이 경우, 위 주소를 복사해서 사용하더라도 일정 시간이 지나면 더 이상 동영상 재생을 할 수 없게 된다.


남은 작업은 Wowza 서버에서 암호화된 주소 값을 인식하도록 하는 것이다. Wowza 서버는 서버 상에 커스텀 모듈을 설치할 수 있도록 지원해주고 있는데, 이 커스텀 모듈을 사용해서 위 기능을 구현할 수 있게 된다. Wowza가 제공하는 서버 API 중 하나인 IMediaStreamNameAliasProvider2 인터페이스를 이용하면 클라이언트가 요청한 변경된 스트림 이름을 실제 스트림 이름으로 변경해 줄 수 있다. 아래 코드는 이 인터페이스를 이용한 구현 예를 보여주고 있다.


public class RaconAliasModule extends ModuleBase implements

        IMediaStreamNameAliasProvider2 {


    public void onAppStart(IApplicationInstance appInstance) {

        appInstance.setStreamNameAliasProvider(this);

        getLogger().info(

                "RaconAliasModule is registered to " + appInstance.getName());

    }


    public String resolvePlayAlias(IApplicationInstance appInstance,

            String name, IClient client) {

        return resolveAlias(name);

    }


    private String resolveAlias(String name) {

        // name이 암호화된 값

        RequestStreamName rsn = convertAliasToRequestStreamName(name); // name을 복호화 처리

        if (rsn.isTimeout()) {

            return null; // 유효하지 않은 name값. null 리턴

        }

        return si.getRealStreamName(); // 실제 스트림 이름 리턴

    }


    public RequestStreamName convertAliasToStreamInfo(String name) {

        // name을 복호화 처리

        ...

    }


    public String resolvePlayAlias(IApplicationInstance appInstance,

            String name, IHTTPStreamerSession httpSession) {

        return resolveAlias(name);

    }


    public String resolvePlayAlias(IApplicationInstance appInstance,

            String name, RTPSession rtpSession) {

        return resolveAlias(name);

    }

    .... // 동일하게 구현

}


모듈을 구현한 클래스를 구현했다면, 해당 모듈을 jar 만들어서 [wowza디렉토리]/lib에 복사한다. 이 때, 모듈에서 함께 사용하는 외부 jar 파일도 함께 복사해 주는 것도 잊지 말자.


lib에 복사했다면 위 모듈을 적용하고 싶은 Wowza 어플리케이션의 Application.xml 파일에 다음과 같이 Module 정보를 추가해 주면 된다.


<Module>

        <Name>customaccesscontrol</Name>

        <Description>CustomAccessControl</Description>

        <Class>com.custom.wowza.module.CustomAliasModule</Class>

</Module>


모듈을 개발하려면 Wowza 관련 jar 파일이 필요한데, 이 jar 파일들은 [wowza디렉토리]/lib에 위치한다. 이 파일들을 별도로 클래스패스로 잡기 귀찮다면 Wowza IDE를 이용해도 된다. Wowza IDE에 대한 자료는 아래 참고자료 링크를 확인해보기 바란다.


참고자료




  1. nwow 2012.07.09 18:13 신고

    setStreamNameAliasProvider 안하고 왜 안되나 하고 있었는데, 여기서 답을 찾았네요.

    좋은 정보 감사합니다. ~

  2. min 2013.02.12 16:26

    초보라... 자세히 만들어서 주시면 안되나요? 샘플이라도... 부탁드립니다.

    • 최범균 madvirus 2013.02.15 07:08 신고

      위 코드가 거의 샘플입니다.
      누락된 부분은
      1. 실제로 암호화된 URL을 만드는 것과
      2. 암호하된 URL을 복호화하는 convertAliastToStreamInfo(String name) 부분이에요.

      암호화된 URL을 만들어주는 기능은 Wowza가 아닌 웹 서버나 기타 다른 프로그램에서 만들어주셔야 할 것 같구요,

      위 코드에서 만든 Wowza 모듈의 convertAliasToStreamInfo 는 암호화된 이름을 복호화 처리하면 됩니다.

      실제 암/복호화와 관련된 건 자바의 Security API를 찾아보시면 될 것 같습니다.

+ Recent posts