본문으로 바로가기
반응형

1. 개요

사용자에게 영상을 제공하는 기능이 요구사항으로 등장하였다. 처음으로 살펴본 서비스는 AWS에서 제공하는 미디어 서비스였다. 타 플랫폼을 이용하게되면 해당 플랫폼에서 만든 규칙을 따라야하기 때문에 여러가지 제약사항이 발생할 수 있기에 AWS 서비스를 살펴봤었는데, 몇가지 사항들때문에 직접 구현하지 않기로 결정했다.

 

첫 번째로, 개발 기간이 늘어난다. 위 요구사항은 언제든지 변할 가능성이 충분한 요구사항이었고 영상을 제공하는 기능 자체는 매니지드 서비스의 장점으로 인해 손쉽게 구현이 가능하겠지만, 한번도 사용해보지 못한 서비스이기 때문에 개발 기간을 정확하게 산정하기가 어려웠다.

두 번째로, 녹화 방지 등 DRM기능이 필요했다. 하지만 그러한 기능까지 서칭하고 구현하기에는 시간적, 비용적인 무리가 있다고 판단했다.

 

그래서 찾아낸 서비스가 Kollus라는 서비스이다. 온라인 비디오 플랫폼 서비스이며 간단하게 제공하는 여러가지 API들을 사용하면 보다 손쉽게 사용자들에게 영상을 제공해줄 수 있을 것으로 판단됐다. 따라서 Kollus를 통해 영상을 서빙하는 방법에 대해 간단하게 기술해본다.

 

2. Flow

Kollus는 상당히 간단한 흐름을 제공한다. 아래의 흐름만 기억하면 별다른 문제없이 손쉽게 영상재생을 구현할 수 있다.

 

1. 재생을 위한 JWT토큰을 서버에서 발급받는다.

2. 발급받은 JWT토큰을 사용하여 영상 재생을 요청한다.

 

http://v.kr.kollus.com/s?jwt=생성한 JWT&custom_key=사용자 키

 

Kollus는 위와 같은 주소로 접근하게되면 내부적으로 JWT를 디코딩하여 어떠한 영상을 재생할 지 판단하여 그에 맞는 영상을 재생시켜준다. 일반적으로 iframe형태로 삽입해주는데 이는 각각의 요구사항마다 다를수도 있다. (참고로 사용자 키는 Kollus 대시보드의 설정으로 들어가면 확인할 수 있다)

 

3. 영상 재생 JWT

Kollus의 경우 영상을 재생하기 위해서 가장 먼저 JWT토큰을 생성해야한다. 페이로드 부분은 https://support.catenoid.net/display/SUP/JWT 를 참고하면 자세하게 알 수 있으며 본문에서는 필요한 부분만 설명한다. 먼저 JWT는 Header, Payload, Signature 총 3가지 부분으로 이루어져있는데, 이중 Payload부분에 특정한 데이터를 삽입하여 토큰을 생성해줘야한다.

 

- cuid: 영상을 재생하는 사용자의 id이다. 일반적으로 데이터베이스에 저장된 유저의 id를 기반으로 둔다

- expt: 영상 재생 토큰 만료 시간

- exp: JWT 토큰 만료 시간(보통 expt와 동일하게 준다)

- mc: Kollus에서는 영상을 업로드하면 영상마다 media_content_key라는 고유 키가 발급되는데 이값을 넣어줘야 추후 영상 재생 시 어떠한 영상을 재생해야하는지 Kollus측에서 판단할 수 있게 된다.

 

대략적인 형태는 아래와 같다

 

{
    "cuid": "catenoid",
    "expt": 1462931880,
    "mc": [{
        "mckey": "vnCVPVyV"
    }]
}

주의할점은 mc부분에 저렇게 배열형태로 넣어줘야한다는 점이다. 마지막으로 JWT Secret의 경우 Kollus 대시보드에서 설정부분에 들어가면 API접근 토큰내역에 암호화된값이 하나 있는데 해당 값을 사용해주면 된다.

여기까지만 알면 영상을 재생하기까지는 큰 문제 없이 구현할 수 있을 것이다. 다만 관리적인 차원에서 몇 가지 추가적으로 알아둬야 할 점들이 있는데 그에 대해 알아보자.

 

4. 영상 등록/수정/삭제 관련 콜백

이건 Optional이긴 한데 Kollus에 등록된 영상 정보를 데이터베이스에 동기화시킬 필요성이 생길때가 있다. 이를 위해 Kollus측에서도 몇가지의 영상 관련 콜백등을 제공해주며 이를 이용하면 손쉽게 Kollus <-> 데이터베이스간 영상 정보를 동기화 시킬 수 있다. 앞서 이야기하자면 영상을 단순히 Kollus 대시보드에 업로드하는 경우 데이터베이스에 동기화시키지 않았다. 영상은 실제로 채널에 등록이 되었을때만 재생될 수 있으며 대시보드에 잘못올리는 경우도 상당히 많아서 불필요한 Request가 생길것이라 판단했기 때문이다. 따라서 아래의 경우에 대해서만 콜백을 통해 동기화를 하기로 하였다.

 

1. 업로드한 영상을 채널에 등록하는 경우

2. 채널에 등록된 영상을 채널에서 삭제하는 경우

3. 영상을 아예 삭제하는 경우

4. 영상 썸네일 포스터를 수정하는 경우

 

1, 2번의 경우 Kollus 대시보드 - 채널로 들어가서 채널 설정 메뉴 중 채널 운영 정책쪽에 들어간 후 컨텐츠 채널 추가 콜백 URL/컨텐츠 채널 삭제 콜백 URL를 통해 설정할 수 있다. 3, 4번의 경우 설정 - 서비스 계정쪽으로 들어간 후 컨텐츠 업데이트 콜백 URL를 통해 설정할 수 있다.

설정해준 후 1~4에 맞는 행동을 수행하면 콜백이 날라오는데 각 번호마다 update_type이 다르게 날아오며 각 타입은 다음과 같다.

 

channel_join: 영상이 채널에 추가됐을경우
channel_leave: 영상이 채널에서 제거됐을경우
content_delete: 영상이 Kollus 대시보드에서 삭제됐을경우
content_poster: 영상 썸네일이 수정됐을경우

update_type인자로 위 값이 채워져서 날라오기 때문에 원하는 타입별로 분기하여 작업을 하면 된다. 주의할점은 콜백으로 날라오는 모든 값은 Form data로 넘어온다.

 

5. 영상 재생 기록

가끔씩 환불과 같은 CS건을 대응하기 위해 사용자가 어떠한 영상을 정확히 어느정도봤는지 파악이 필요할때가 있다. 이러한 요구사항을 위해 Kollus측에서는 영상 재생에 대한 기록을 Callback으로 보내주는데 우리는 이 정보를 이용할 수 있다. 먼저 몇가지의 선행 지식이 필요하다.

 

1. 영상은 N개의 블럭으로 나눠진다. 예를 들어서 100초짜리 영상을 10개의 블럭으로 나눈다면 10초마다 하나의 블럭을 차지한다. 블럭 개수 설정은 Kollus측에 직접 요청해야한다.

2. LMS콜백을 설정하면 사용자가 영상을 시청하는 중간마다 Kollus측에서 정해진 주소로 콜백을 보내준다. LMS콜백 또한 Kollus측에 직접 요청해야한다.

 

다음으로 콜백의 형태를 한번 살펴보자.

 

    {
        "user_info": {
            "content_provider_key":"providerkey",
            "client_user_id":"181353",
            "player_id":"playerid",
            "host_name":"host.video.kr.kollus.com",
            "device":"x86_64",
            "os":"MAC",
            "version":"19.4.0"
        },
        "content_info":{
            "duration":"1461",
            "encoding_profile":"wanted-pc1-high",
            "media_content_key":"mediacontentkey",
            "channel_key":"channelkey",
            "real_playtime":"151",
            "playtime":"151",
            "playtime_percent":"10",
            "start_at":"1593586044",
            "last_play_at":"1225",
            "runtime":"2245",
            "showtime":"151",
            "serial":"16"
        },
        "block_info":{
        "block_count":"30",
        "blocks":{
            "b0":"0",
            "t0":"0",
            "p0":"0",
            "b1":"0",
            "t1":"0",
            "p1":"0",
            "b2":"0",
            "t2":"0",
            "p2":"0",
            "b3":"0",
            "t3":"0",
            "p3":"0",
            "b4":"0",
            "t4":"0",
            "p4":"0",
            "b5":"0",
            "t5":"0",
            "p5":"0",
            "b6":"0",
            "t6":"0",
            "p6":"0",
            "b7":"0",
            "t7":"0",
            "p7":"0",
            "b8":"0",
            "t8":"0",
            "p8":"0",
            "b9":"0",
            "t9":"0",
            "p9":"0",
            "b10":"0",
            "t10":"0",
            "p10":"0",
            "b11":"0",
            "t11":"0",
            "p11":"0",
            "b12":"0",
            "t12":"0",
            "p12":"0",
            "b13":"0",
            "t13":"0",
            "p13":"0",
            "b14":"0",
            "t14":"0",
            "p14":"0",
            "b15":"0",
            "t15":"0",
            "p15":"0",
            "b16":"0",
            "t16":"0",
            "p16":"0",
            "b17":"0",
            "t17":"0",
            "p17":"0",
            "b18":"0",
            "t18":"0",
            "p18":"0",
            "b19":"0",
            "t19":"0",
            "p19":"0",
            "b20":"0",
            "t20":"0",
            "p20":"0",
            "b21":"1",
            "t21":"3",
            "p21":"6",
            "b22":"1",
            "t22":"48",
            "p22":"100",
            "b23":"1",
            "t23":"48",
            "p23":"100",
            "b24":"1",
            "t24":"48",
            "p24":"100",
            "b25":"1",
            "t25":"4",
            "p25":"8",
            "b26":"0",
            "t26":"0",
            "p26":"0",
            "b27":"0",
            "t27":"0",
            "p27":"0",
            "b28":"0",
            "t28":"0",
            "p28":"0",
            "b29":"0",
            "t29":"0",
            "p29":"0"
        }
    }
}

조금 복잡해보일수도 있는데 막상 살펴보면 크게 복잡하진 않다. (겁먹지말자) 먼저 user_info내부에 환경에 대한 정보가 들어있고 content_info내부에 재생하는 영상에 대한 정보가 들어있다. 마지막으로 blocks에 우리에게 필요한 재생정보가 담겨있다. 여기서 몇가지 사용할만한 것들을 추려보면 아래와 같다.

 

- user-info
   - client_user_id: 유저 아이디(영상 재생 토큰을 생성할 때 cuid에 넣어준 값)

- content_info
   - media_content_key: Kollus 동영상 고유 키
   - channel_key: Kollus 채널 키
   - playtime: 총 재생 시간(초)
   - start_at: 재생 시작 시간(초)

- blocks
   - b
   - t
   - p

b, t, p의 경우 예를 들어 블럭이 총 30개인 경우 b0~b29, t0~t29, p0~p29 총 30개의 b, t, p로 구성되어 보내진다. 각 값의 뜻은 다음과 같다.

 

- b: 블럭 시청 여부(false: 0, true: 1)

- t: 블럭 시청 시간

- p: 블럭 시청 퍼센트

 

예를 들어 b0: 1, t0: 10, p:22라는 값을 해석해보자면 첫번째 블럭을 시청했고, 시청 시간은 10초이며, 10초는 해당 블럭의 22%에 해당한다고 해석하면 된다. 주의할점은 이 콜백의 경우 기본적으로 이전 시청 정보를 누적하여 보내오지만(이미 시청한 블럭의 정보도 같이 넘어옴) 만약 플레이어를 종료하고 다시 실행한다면 시청 정보가 초기화되어 날라온다는 점이다.

그렇다면 기록은 어떠한 형태로 저장해야할까? 하나의 영상에 대해서는 무조건 시청 정보를 중첩하여 업데이트 해줘야할까? 이에 대해서는 정책마다 다르겠지만 우리는 새로운 플레이어를 통해 시청하는 정보는 새로운 로우로 저장하는게 맞다고 판단했다. 한번에 쭉 시청하는 정보와 다음에 새롭게 시청하는 정보를 분리하여 좀 더 세밀하게 유저의 시청 정보를 기록하기 위함이다. 따라서 아래와 같은 정책을 세웠다.

 

1. 히스토리가 존재하지 않는 경우
   - 처음 영상을 재생하는 것이므로 새로운 로우를 추가한다.

2. 히스토리가 존재하고 start_at(실행 시간)이 같은 경우
   - 같은 플레이어로 지속적으로 시청하고 있는 것이므로 total_play_time에 총 재생 시간(초)를 누적하고, block_info를 업데이트한다.

3. 히스토리가 존재하고 start_at(실행 시간)이 다른 경우
   -새로운 플레이어로 시청하는 것이므로 새로운 로우를 추가한다.

 

여기서 시청 기록 콜백에 추가적인 정보를 전달해야할때가 있다. 케이스를 하나 예로 들어보자면, 영상을 테스트 서버에서 재생했는지 실 서버에서 재생했는지 판단해야하는 경우이다.

 

http://v.kr.kollus.com/s?jwt=생성한 JWT&custom_key=사용자 키

 

아까 2번에서 위와 같은 주소를 통해 재생을 한다고 했는데 여기에 uservalue0=production과 같은 Query String을 추가적으로 넣어주면 된다.

 

http://v.kr.kollus.com/s?jwt=생성한 JWT&custom_key=사용자 키&uservalue0=production

 

그러면 콜백에 uservalue로 설정한 값이 포함되어 날아오게되며 해당 값을 통해 어떠한 테스트 서버인지 실 서버인지 판단할 수 있다.

 

6. 기타 API

막상 구현을 하다보면 추가적으로 정보를 가져와야하는 경우가 발생하는데 http://api.dev.kollus.com/group/media 를 통해 살펴보자.

 

7. 후기

일단 Kollus라는 서비스를 처음 사용해보는 입장으로써 느낀 생각을 간단하게 정리해보자면 다음과 같다.

 

좋았던 점

- 구현이 간단하다. 영상 재생 토큰을 발급하고 해당 토큰을 활용하여 iframe내부에 URL형태로 끼워넣기만 하면 된다.

- (어찌보면 당연히 제공되어야 하는 부분이겠지만) 영상을 재생할 때 끊김 현상이나 기타 불편 사항이 없다.

 

아쉬운 점

- 간혹 버그가 있다. 우리는 현재 영상을 올리면 기본적으로 생성되는 썸네일 이미지를 사용하지 않고 새롭게 디자인팀에서 제공해주는 이미지를 업로드하고 있는데, JPG가 아닌 JPEG의 경우 API를 통해 호출했을 때 바로 갱신이 되지 않는다. 현재 문의중인 상태이니 조만간 답이 나오지 않을까.

- API문서의 가독성이 떨어진다. 한눈에 어떠한 흐름으로 구현해야하는지 명확하지 않다. API의 내용 또한 알아보기 힘들다.

- API콜의 구조가 복잡하다. 특정 정보를 얻기 위해 여러번의 API콜을 날려야하는 경우가 종종 있다.

 

아쉬운 점도 있지만 분명히 생산 속도를 올려주는 괜찮은 서비스라고 생각한다. 의식의 흐름 기법을 통해 글을 작성해봤는데, 비슷한 요구사항에 직면한 개발자들에게 작게나마 도움이 되었으면 한다.

반응형