본문으로 건너뛰기

DRM 다운로드 콜백

Kollus VOD는 콘텐츠의 업로드부터 재생까지 전체 워크플로우에 대해 콜백(Callback) 연동을 지원합니다.
고객사는 콜백 기능을 사용하여 주요 이벤트 발생 시 지정된 URL로 실시간 HTTP 알림을 수신할 수 있으며, 이를 통해 외부 시스템과의 데이터를 동기화하거나 업무 프로세스를 자동화할 수 있습니다.

DRM 다운로드 콜백(DRM Callback)은 최종 사용자(End user)가 Kollus 플레이어에서 콘텐츠 다운로드를 요청하거나, 이미 다운로드된 콘텐츠를 오프라인 상태에서 재생할 때 호출됩니다. 이 콜백을 통해 다운로드 권한 및 DRM 정책(만료 시각 등)을 서버에서 동적으로 제어할 수 있습니다.


유의 사항

  • 데이터 규격 준수: 응답 데이터의 타입이나 값이 지정된 범위를 벗어날 경우, 콘텐츠 다운로드 및 재생이 즉시 차단됩니다.
  • 설정 되돌리기 불가: 잘못된 설정값 전송으로 인해 발생한 사용 제한(만료 처리 등)은 이전 상태로 되돌릴 수 없으므로 정확한 값을 전송해야 합니다.
  • 서버 가용성: 고객사 콜백 서버가 응답하지 않거나 응답 데이터가 형식에 맞지 않는 경우, 이후의 콜백 프로레스가 정상적으로 진행되지 않습니다.

콜백 설정 방법

콜백 URL은 Kollus VOD 콘솔에서 설정할 수 있습니다.
상세한 설정 위치는 콜백 설정 - DRM 다운로드 콜백 문서를 참고하세요.


콜백 흐름

DRM 다운로드 콜백 흐름
⚠️주의
  • 응답 형식: 콜백 서버의 응답은 반드시 JWT(JSON Web Token) 형식으로 인코딩되어야 합니다.
  • 헤더 설정: JWT 헤더 내에 사용자 키(X-KOLLUS-USERKEY)를 반드시 포함해야 합니다.
    사용자 키는 Kollus VOD 콘솔 > [서비스 계정] > [사용자 키]에서 확인할 수 있습니다.
  • 데이터 타입 준수: 응답 JSON 페이로드 내의 모든 정수형 필드(expiration_date, result, content_expired 등)은 integer 타입으로 전달해야 합니다.
    ("1"처럼 string 타입으로 전송 시 처리 실패)

콜백 유형

DRM 다운로드 콜백은 목적에 따라 다음 세 가지 유형으로 구분됩니다. 각 유형에 따라 응답해야 하는 파라미터 규격이 다릅니다.

  • kind1 (다운로드 승인): 사용자가 다운로드 버튼을 눌렀을 때, 서버에서 승인 여부를 결정합니다.
  • kind2 (다운로드 완료 통보): 콘텐츠 다운로드가 100% 완료된 시점에 서버로 결과를 알립니다.
  • kind3 (오프라인 재생 권한 확인): 다운로드된 콘텐츠를 재생할 때마다 재생 가능 여부를 서버에 확인합니다.

요청

전송 방식

  • HTTP Method: POST
  • Content-Type: application/x-www-form-urlencoded
  • Data Format: items 파라미터에 JSONArray 형태의 JSON 문자열을 담아 전송합니다.

kind1, kind2 요청 파라미터

items

파라미터타입필수 여부설명
kindintegerDRM 다운로드 콜백 유형
  • 1: 다운로드 승인
  • 2: 다운로드 완료 통보
client_user_idstring시청자 ID(JWT 생성 시 입력한 client_user_id와 동일)
player_idstring기기 고유 식별자
hardware_idstring-기기 시리얼 넘버(PC에서만 제공)
device_namestring-기기 모델명
media_content_keystring채널 내 콘텐츠의 고유 식별자
uservaluesJSON string-고객사 정의 값(uservalue0~uservalue9)
localtimeinteger-기기 UTC 시간

kind1, kind2 items 예시

[
{
"kind": 1,
"media_content_key" : "XXXXXX",
"client_user_id": "XXXXXXX",
"player_id": "XXXXXXXXXXXXXXXX",
"device_name": "XXXXX",
"uservalues": {
"uservalue0": "value0"
}
}
]

kind3 요청 파라미터

다운로드된 콘텐츠를 재생할 때 호출되며, 오프라인 환경에서의 유효성을 검증합니다.

items

파라미터타입필수 여부설명
kindintegerDRM 다운로드 콜백 유형
  • 3: 오프라인 재생 권한 확인
session_keystring만료 시간 연장 시 검증용 세션 키(content_expire_reset 요청 시, 동일한 session_key가 사용되었는지 확인함)
client_user_idstring시청자 ID(JWT 생성 시 입력한 client_user_id와 동일)
player_idstring기기 고유 식별자
hardware_idstring-기기 시리얼 넘버(PC에서만 제공)
device_namestring-기기 모델명
media_content_keystring채널 내 콘텐츠의 고유 식별자
start_atinteger전송 요청 시각(Unix Timestamp → localtime)
uservaluesJSON string-고객사 정의 값(uservalue0~uservalue9)
content_expiredinteger-재생 만료 여부
  • 0: 재생 가능
  • 1: 재생 차단(만료)
check_expiredinteger-체크 유효 시각 만료 여부
  • 0: 가능
  • 1: 만료
reset_reqinteger-일괄 업데이트 요청 여부
  • 0: 일반(기본값)
  • 1: 일괄 업데이트
expiration_dateinteger-재생 만료 시각(Unix Timestamp)
localtimeinteger-기기 UTC 시간

kind3 items 예시

[
{
"kind": 3,
"session_key" : "XXXXXX",
"media_content_key" : "XXXXXX",
"client_user_id": "XXXXXXX",
"player_id": "XXXXXXXXXXXXXXXX",
"device_name": "XXXXX",
"uservalues": {
"uservalue1": "value1"
}
}
]

uservalues 예시

{
"uservalue0": "강의코드01",
"uservalue1": "상품코드02",
"uservalue9": "생성코드03"
}

응답

전송 방식

DRM 다운로드 콜백 응답은 보안을 위해 반드시 JWT(JSON Web Token) 형식으로 인코딩되어야 합니다.

  • Header: X-KOLLUS-USERKEY: {사용자 키}
  • Content-Type: text/plain
  • 페이로드 구조: data 필드 내에 개별 콘텐츠 응답 객체를 담은 배열 구조로 구성합니다.
    {
    "data": [
    {...},
    {...}
    ]
    }

만료 옵션 규격

⚠️주의

한 번 설정된 만료 옵션은 Kollus 시스템 내에서 수정하거나 회수할 수 없습니다. 반드시 정확한 Unix Timestamp 값을 전송하세요.

expiration_count

  • 설명: 재생 횟수 제한
  • 타입: integer
  • 값 범위: 0(제한 없음)~1000

expiration_date

  • 설명: 재생 만료 시각(Uinx Timestamp)
  • 타입: integer
  • 예시: 2014-03-03 05:45:30 GMT → 1393825531
  • 값 범위: 0(제한 없음)~1893455999(2029-12-31 23:59:59)

expiration_playtime

  • 설명: 재생 시간 제한(단위: 초)
  • 타입: integer
  • 값 범위: 0(제한 없음), 60(60초)~604800(7일)

kind1 응답 필드

필드타입필수 여부기본값설명
kindinteger-DRM 다운로드 콜백 유형
  • 1: 다운로드 승인
media_content_keystring-채널 내 콘텐츠의 고유 식별자
expiration_dateinteger--재생 만료 시각(Unix Timestamp)
expiration_countinteger--재생 횟수 제한
예: 10 → 10회 재생 가능
expiration_playtimeinteger--재생 시간 제한(단위: 초)
예: 60 → 60초 재생 가능
expiration_playtime_typeinteger--
  • 1: 실제 재생 중인 시간(playtime) 기준으로 차감
resultinteger-승인 결과
  • 0: 실패/차단
  • 1: 성공/허용
messagestring--result0일 때 표시할 메시지
미입력 시 Kollus 기본 오류 메시지가 표시됩니다.
expiration_refresh_popupinteger-0만료 시 갱신 알림 표시 여부
  • 0: 표시 안 함
  • 1: 표시
vmcheckinteger-1가상머신 체크 여부(Html5 Player for PC에서만 사용 가능)
  • 0: 체크 안 함
  • 1: 체크함(기본값)
check_abuseinteger-0다운로드된 콘텐츠(오프라인) 재생 시 kind3 항상 호출 여부
  • 0: 안 함(오프라인 재생 가능)
  • 1: 항상 확인
offline_bookmark.downloadinteger-0오프라인 상태에서 북마크 다운로드 사용 여부
콘텐츠 다운로드 시점의 데이터만 다운로드되며, 이후 서버와 동기화되지 않습니다.
  • 0: 사용 안 함
  • 1: 사용
offline_bookmark.readonlyinteger-0오프라인 상태에서 북마크 추가/삭제 사용 여부
  • 0: 사용 안 함
  • 1: 사용

kind1 응답 예시

{
"data" : [
{
"kind": 1,
"media_content_key": "XXXXXX",
"expiration_date": 1402444800,
"expiration_playtime": 1800,
"result": 1
}
]
}

kind2 응답 필드

필드타입필수기본값설명
kindinteger-DRM 다운로드 콜백 유형
  • 2: 다운로드 완료 통보
media_content_keystring-채널 내 콘텐츠의 고유 식별자
content_deleteinteger-0다운로드된 콘텐츠 즉시 삭제 여부
  • 0: 삭제하지 않음
  • 1: 삭제
messagestring--result0이거나 content_delete1일 때 표시할 메시지
미입력 시 Kollus 기본 오류 메시지가 표시됩니다.
check_expiration_dateinteger-0체크 유효 만료 시각(Unix Timestamp)
  • 0: 제한 없음
resultinteger-처리 결과
  • 0: 비정상(다른 옵션 무시됨)
  • 1: 정상

kind2 응답 예시

{
"data" : [
{
"kind": 2,
"media_content_key": "XXXXXX",
"content_delete": 1,
"result": 1
}
]
}

kind3 응답 필드

필드타입필수기본값설명
kindinteger-3
session_keystring--DRM 다운로드 콜백 유형
  • 3: 오프라인 재생 권한 확인
media_content_keystring-채널 내 콘텐츠의 고유 식별자
start_atinteger-요청에 포함된 start_at 값을 그대로 반환
content_expiredinteger-0다운로드된 콘텐츠 강제 만료 처리
content_expire_reset 옵션을 통해 되돌릴 수 있습니다.
  • 0: 재생 가능
  • 1: 재생 차단
content_deleteinteger-0다운로드된 콘텐츠 삭제 여부
content_expired 값과 관계없이, 이 옵션이 설정되어 있으면 다운로드 콘텐츠는 삭제됩니다.
  • 0: 삭제하지 않음
  • 1: 삭제
content_expire_resetinteger-0만료된 권한 초기화
  • 0: 추가 작업 안 함
  • 1: 만료된 콘텐츠 권한 초기화
expiration_dateinteger--재생 만료 시각(Unix Timestamp)
content_expire_reset1인 경우 재설정됩니다.
expiration_countinteger--재생 횟수 제한
content_expire_reset1인 경우 재설정됩니다.
예: 10 → 10회 재생 가능
expiration_playtimeinteger--재생 시간 제한(단위: 초)
content_expire_reset1인 경우 재설정됩니다.
예: 3600 → 1시간 재생 가능
resultinteger-처리 결과
  • 0: 비정상(다른 옵션 무시됨)
  • 1: 정상
messagestring--result0이거나 content_delete 또는 content_expired1일 때 표시할 메시지
미입력 시 Kollus 기본 오류 메시지가 표시됩니다.
check_abuseinteger-0다운로드된 콘텐츠(오프라인) 재생 시 kind3 항상 호출 여부
  • 0: 안 함(오프라인 재생 가능)
  • 1: 항상 확인
check_expiration_dateinteger-0체크 유효 만료 시각(Unix Timestamp)
content_expire_reset1인 경우 재설정됩니다.
  • 0: 제한 없음
ℹ️참고

content_expired1인 경우, content_expire_reset1로 설정해도 해당 옵션은 무시됩니다.

kind3 응답 예시

{
"data": [
{
"kind": 3,
"session_key": "XXXXXX",
"media_content_key": "XXXXXX",
"start_at": 140000000,
"result": 1,
"content_expired": 1,
"content_delete": 1,
"content_expire_reset": 1,
"expiration_date": 1402444800,
"expiration_count": 10,
"expiration_playtime": 3600
}
]
}

device_name 상세

device_name은 플레이어가 호출될 때 기기를 식별하기 위해 전달되는 정보로, 운영체제별로 다음과 같은 규칙을 따릅니다.

Android

Android 앱에서는 기기의 Build.DEVICEBuild.MODEL/(슬래시)로 조합한 문자열을 사용합니다.

Build.DEVICE/Build.MODEL

iOS

iOS 앱에서는 iOS에서 제공하는 device_name을 사용합니다.

iOS device_name
디바이스device_name
iPhone1,1iPhone
iPhone1,2iPhone 3G
iPhone2,1iPhone 3GS
iPhone3,1iPhone 4 (GSM)
iPhone3,3iPhone 4 CDMA
iPhone4,1iPhone 4S
iPhone5,1iPhone 5 A1428
iPhone5,2iPhone 5 A1429
iPhone5,3iPhone 5c A1456/A1532
iPhone5,4iPhone 5c A1507/A1516/A1529
iPhone6,1iPhone 5s A1433/A1453
iPhone6,2iPhone 5s A1457/A1518/A1530
iPhone7,1iPhone 6 Plus
iPhone7,2iPhone 6
iPhone8,1iPhone 6s
iPhone8,2iPhone 6s Plus
iPhone8,4iPhone SE
iPhone9,1iPhone 7 A1660/A1779/A1780
iPhone9,2iPhone 7 Plus A1661/A1785/A1786
iPhone9,3iPhone 7 A1778
iPhone9,4iPhone 7 Plus A1784
iPhone10,1iPhone 8 A1863/A1906
iPhone10,2iPhone 8 Plus A1864/A1898
iPhone10,3iPhone X A1865/A1902
iPhone10,4iPhone 8 A1905
iPhone10,5iPhone 8 Plus A1897
iPhone10,6iPhone X A1901
iPad1,1iPad
iPad2,1iPad 2 WiFi
iPad2,2iPad 2 (GSM)
iPad2,3iPad 2 CDMA
iPad2,4iPad 2 WiFi (revised)
iPad2,5iPad mini WiFi
iPad2,6iPad mini A1454
iPad2,7iPad mini A1455
iPad3,1iPad 3rd gen (WiFi)
iPad3,2iPad 3rd gen (WiFi+LTE Verizon)
iPad3,3iPad 3rd gen (WiFi+LTE AT&T)
iPad3,4iPad 4th gen (WiFi)
iPad3,5iPad 4th gen A1459
iPad3,6iPad 4th gen A1460
iPad4,1iPad Air WiFi
iPad4,2iPad Air WiFi+LTE
iPad4,3iPad Air Rev
iPad4,4iPad mini 2 WiFi
iPad4,5iPad mini 2 WiFi+LTE
iPad4,6iPad mini 2 Rev
iPad4,7iPad mini 3 WiFi
iPad4,8iPad mini 3 A1600
iPad4,9iPad mini 3 A1601
iPad5,1iPad mini 4 WiFi
iPad5,2iPad mini 4 WiFi+LTE
iPad5,3iPad Air 2 WiFi
iPad5,4iPad Air 2 WiFi+LTE
iPad6,3iPad Pro 9.7 inch WiFi
iPad6,4iPad Pro 9.7 inch WiFi+LTE
iPad6,7iPad Pro 12.9 inch WiFi
iPad6,8iPad Pro 12.9 inch WiFi+LTE
iPad6,11iPad 9.7 Inch 5th Gen WiFi Only
iPad6,12iPad 9.7 Inch 5th Gen WiFi/Cellular
iPad7,1iPad Pro 12.9 inch A1670
iPad7,2iPad Pro 12.9 inch A18219
iPad7,3iPad Pro 10.5 inch A1701
iPad7,4iPad Pro 10.5 inch A1709
iPad7,5iPad 6th gen A1893
iPad7,6iPad 6th gen A1954
iPod1,1iPod touch
iPod2,1iPod touch 2nd gen
iPod3,1iPod touch 3rd gen
iPod4,1iPod touch 4th gen
iPod5,1iPod touch 5th gen
iPod7,1iPod touch 6th gen

Java/Spring 사용 예제

JWT 인코딩 예제

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.*;
import com.fasterxml.jackson.databind.ObjectMapper;

public class KollusJwtEncoder {

private final ObjectMapper objectMapper = new ObjectMapper();

public String encode(Map<String, Object> payload, String key) throws Exception {
Map<String, String> header = new HashMap<>();
header.put("alg", "HS256");
header.put("typ", "JWT");

String headerJson = objectMapper.writeValueAsString(header);
String payloadJson = objectMapper.writeValueAsString(payload);

String encodedHeader = Base64.getUrlEncoder().withoutPadding()
.encodeToString(headerJson.getBytes(StandardCharsets.UTF_8));
String encodedPayload = Base64.getUrlEncoder().withoutPadding()
.encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8));
String unsignedToken = encodedHeader + "." + encodedPayload;

Mac hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
hmac.init(secretKey);
byte[] signatureBytes = hmac.doFinal(unsignedToken.getBytes(StandardCharsets.UTF_8));

String signature = Base64.getUrlEncoder().withoutPadding()
.encodeToString(signatureBytes);

return unsignedToken + "." + signature;
}
}

Spring Boot DRM 다운로드 콜백 예제

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.time.Instant;
import java.util.*;

@SpringBootApplication
@RestController
public class KollusDrmCallbackApplication {

private static final String KOLLUS_KEY = "**사용자키**";
private static final String JWT_KEY = "**보안키**";

private final ObjectMapper objectMapper = new ObjectMapper();
private final KollusJwtEncoder jwtEncoder = new KollusJwtEncoder();

/**
* DRM 다운로드 콜백
* 엔드포인트: /drm/callback
* HTTP Method: POST
* Content-Type: application/x-www-form-urlencoded
* Parameter: items (JSONArray string)
*/
@PostMapping("/drm/callback")
public ResponseEntity<String> drmCallback(@RequestParam("items") String items) {
try {
// items 파싱
List<Map<String, Object>> itemsList = objectMapper.readValue(items, List.class);
List<Map<String, Object>> resultData = new ArrayList<>();

// 각 item 처리
for (Map<String, Object> item : itemsList) {
Integer kind = (Integer) item.get("kind");
String mediaContentKey = (String) item.get("media_content_key");
String clientUserId = (String) item.get("client_user_id");

Map<String, Object> result = switch (kind) {
case 1 -> processKind1(mediaContentKey, clientUserId);
case 2 -> processKind2(mediaContentKey, clientUserId);
case 3 -> processKind3(mediaContentKey, clientUserId, item);
default -> createErrorResponse(kind, mediaContentKey, "Invalid kind");
};

resultData.add(result);
}

// JWT 응답 생성
Map<String, Object> payload = new HashMap<>();
payload.put("data", resultData);
String jwtToken = jwtEncoder.encode(payload, JWT_KEY);

// 응답 헤더 설정
HttpHeaders headers = new HttpHeaders();
headers.set("X-Kollus-UserKey", KOLLUS_KEY);
headers.setContentType(MediaType.TEXT_PLAIN);

return ResponseEntity.ok().headers(headers).body(jwtToken);

} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.internalServerError().body("Error processing callback");
}
}

/**
* kind1 처리 (다운로드 승인)
*/
private Map<String, Object> processKind1(String mediaContentKey, String clientUserId) {
// TODO: DB에서 사용자 및 콘텐츠 권한 확인

Map<String, Object> response = new HashMap<>();
response.put("kind", 1);
response.put("media_content_key", mediaContentKey);
response.put("result", 1);
response.put("expiration_date", Instant.now().getEpochSecond() + 86400); // 1일
response.put("expiration_count", 3);
response.put("expiration_playtime", 1800); // 30분
response.put("vmcheck", 1);
response.put("check_abuse", 0);

return response;
}

/**
* kind2 처리 (다운로드 완료 통보)
*/
private Map<String, Object> processKind2(String mediaContentKey, String clientUserId) {
// TODO: DB에 다운로드 완료 기록

Map<String, Object> response = new HashMap<>();
response.put("kind", 2);
response.put("media_content_key", mediaContentKey);
response.put("result", 1);
response.put("content_delete", 0);
response.put("check_expiration_date", 0);

return response;
}

/**
* kind3 처리 (오프라인 재생 권한 확인)
*/
private Map<String, Object> processKind3(String mediaContentKey, String clientUserId,
Map<String, Object> item) {
// TODO: DB에서 재생 권한 및 만료 여부 확인

Map<String, Object> response = new HashMap<>();
response.put("kind", 3);
response.put("media_content_key", mediaContentKey);
response.put("start_at", item.get("start_at"));
response.put("result", 1);
response.put("content_expired", 0);
response.put("content_delete", 0);
response.put("content_expire_reset", 0);
response.put("check_abuse", 0);
response.put("check_expiration_date", 0);

if (item.containsKey("session_key")) {
response.put("session_key", item.get("session_key"));
}

return response;
}

/**
* 에러 응답 생성
*/
private Map<String, Object> createErrorResponse(Integer kind, String mediaContentKey, String message) {
Map<String, Object> response = new HashMap<>();
response.put("kind", kind);
response.put("media_content_key", mediaContentKey);
response.put("result", 0);
response.put("message", message);
return response;
}

public static void main(String[] args) {
SpringApplication.run(KollusDrmCallbackApplication.class, args);
}
}

예제 코드