본문으로 건너뛰기

DRM 다운로드 콜백

Kollus VOD는 사용자가 콘텐츠를 기기에 다운로드하거나, 저장된 파일을 오프라인 상태에서 재생할 때 보안 권한을 검증하는 DRM 다운로드 콜백 기능을 지원합니다. 이를 통해 다운로드 허용 여부, 재생 가능 횟수, 유효 기간 등 정교한 DRM 정책을 실시간으로 제어할 수 있습니다.


유의 사항

  • 데이터 무결성: 응답 데이터의 타입이 일치하지 않거나 범위를 벗어날 경우, 다운로드 및 재생이 즉시 차단됩니다.
  • 설정 회수 불가: 잘못 전송된 만료 일시(expiration_date) 등의 설정값은 Kollus 시스템 내에서 임의로 수정하거나 취소할 수 없으므로 정확한 값을 전송해야 합니다.
  • 서버 가용성: 콜백 서버 응답 지연이나 장애 발생 시 서비스 이용이 제한되므로 안정적인 서버 환경 구성을 권장합니다.

콜백 설정 방법

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


콜백 흐름

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

콜백 유형

DRM 콜백은 처리 목적에 따라 세 가지 유형으로 구분됩니다.

  • kind1 (다운로드 승인): 사용자가 다운로드 버튼을 클릭 시 서버에서 권한 및 DRM 정책을 할당합니다.
  • kind2 (다운로드 완료 통보): 기기에 파일 저장이 100% 완료된 시점에 결과 데이터를 전송합니다.
  • kind3 (오프라인 재생 권한 확인): 저장된 콘텐츠를 재생할 때마다 재생 권한 및 만료 여부를 확인합니다.

요청 규격

전송 방식

  • 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-시청 기기 하드웨어 시리얼 넘버 (Windows 환경 등 식별 가능한 값이 존재하는 경우 제공)
device_namestring-시청 기기 모델명
media_content_keystring미디어 콘텐츠 키
localtimeinteger-시청 기기 기준 UTC 시간
uservaluesJSON string-고객사 정의 값 (uservalue0~uservalue99)

kind1, kind2 items 예시

[
{
"kind": 1,
"media_content_key" : "{MEDIA_CONTENT_KEY}",
"client_user_id": "{END_USER_ID}",
"player_id": "{PLAYER_ID}",
"device_name": "{DEVICE_NAME}",
"uservalues": {
"uservalue0": "value0"
}
}
]

kind3 요청 파라미터

items

파라미터타입필수 여부설명
kindintegerDRM 다운로드 콜백 유형
  • 3: 오프라인 재생 권한 확인
session_keystring만료 일시 갱신용 세션 키 (content_expire_reset 요청 시 정합성 검증에 사용)
client_user_idstring시청자 ID (JWT 생성 시 입력한 client_user_id)
player_idstring시청 기기 고유 식별자
hardware_idstring-시청 기기 하드웨어 시리얼 넘버 (Windows 환경 등 식별 가능한 값이 존재하는 경우 제공)
device_namestring-시청 기기 모델명
media_content_keystring미디어 콘텐츠 키
start_atinteger전송 요청 시점의 로컬 시각
uservaluesJSON string-고객사 정의 값 (uservalue0~uservalue99)
content_expiredinteger-재생 만료 여부
  • 0: 재생 가능
  • 1: 재생 차단 (만료)
check_expiredinteger-체크 유효 시각 만료 여부
  • 0: 정상
  • 1: 만료
reset_reqinteger-일괄 업데이트 요청 여부
  • 0: 일반 업데이트 (기본값)
  • 1: 일괄 업데이트
expiration_dateinteger-재생 만료 일시 (Unix Timestamp)
localtimeinteger-시청 기기 기준 UTC 시간

kind3 items 예시

[
{
"kind": 3,
"session_key" : "{SESSION_KEY}",
"media_content_key" : "{MEDIA_CONTENT_KEY}",
"client_user_id": "{END_USER_ID}",
"player_id": "{PLAYER_ID}",
"device_name": "{DEVICE_NAME}",
"uservalues": {
"uservalue1": "value1"
}
}
]

uservalues 예시

{
"uservalue0": "class_code_01",
"uservalue1": "product_code_02",
"uservalue99": "custom_code_03"
}

응답 규격

전송 방식

DRM 다운로드 콜백 응답은 데이터 보안과 무결성을 위해 반드시 JWT(JSON Web Token) 형식으로 인코딩하여 반환해야 합니다.

  • Header: X-KOLLUS-USERKEY: {사용자 키}
  • Content-Type: text/plain
  • 페이로드(Payload) 구조: data 필드 내에 개별 콘텐츠 응답 객체를 포함하는 배열 구조
    {
    "data": [
    { "kind": 1, "media_content_key": "...", "result": 1, ... },
    { "kind": 1, "media_content_key": "...", "result": 1, ... }
    ]
    }

DRM 만료 옵션 규격

⚠️주의

설정된 만료 옵션은 Kollus 시스템에 기록된 후 수정 또는 회수가 불가능합니다. 반드시 정확한 Unix Timestamp 값을 할당하세요.

옵션타입허용 범위설명
expiration_countinteger0 (제한 없음) ~ 1000재생 허용 횟수
expiration_dateinteger0 (제한 없음) ~ 2145916799 (2037-12-31 23:59:59)재생 만료 일시 (Unix Timestamp)
expiration_playtimeinteger0 (제한 없음), 60 (60초) ~ 604800 (7일)재생 제한 시간 (초)

kind1 응답 필드

사용자의 다운로드 요청에 대해 서버에서 승인 여부를 결정하고, 해당 기기에 저장될 콘텐츠의 DRM 정책을 할당합니다.

data 항목

필드타입필수 여부기본값설명
kindinteger-DRM 다운로드 콜백 유형
  • 1: 다운로드 승인
media_content_keystring-미디어 콘텐츠 키
expiration_dateinteger--재생 만료 일시 (Unix Timestamp)
expiration_countinteger--재생 허용 횟수 (예: 10 → 10회 재생 가능)
expiration_playtimeinteger--재생 제한 시간 (예: 60 → 60초, 3600 → 1시간 재생 가능)
expiration_playtime_typeinteger--재생 시간 차감 방식
  • 1: 실제 재생 시간 기준 차감
resultinteger-승인 결과
  • 0: 실패/차단
  • 1: 성공/허용
messagestring--재생 차단(result: 0) 시 플레이어 화면에 노출할 안내 메시지 (미입력 시 Kollus 기본 오류 메시지 노출)
expiration_refresh_popupinteger-0만료 시 갱신 알림 노출 여부
  • 0: 미노출
  • 1: 노출
vmcheckinteger-1(Html5 Player for PC 전용) 가상머신(VM) 환경 재생 허용 여부
  • 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": "{MEDIA_CONTENT_KEY}",
"expiration_date": 1402444800,
"expiration_playtime": 1800,
"result": 1
}
]
}

kind2 응답 필드

콘텐츠 다운로드 프로세스가 성공적으로 종료되었음을 서버에 알리고, 서버는 응답을 통해 해당 콘텐츠의 유효성을 최종 확정합니다.

data 항목

필드타입필수기본값설명
kindinteger-DRM 다운로드 콜백 유형
  • 2: 다운로드 완료 통보
media_content_keystring-미디어 콘텐츠 키
content_deleteinteger-0다운로드 완료 직후 파일 삭제 여부
  • 0: 삭제하지 않음
  • 1: 삭제
messagestring--재생 차단(result: 0 또는 content_expired: 1) 시 플레이어에 노출할 안내 메시지 (미입력 시 Kollus 기본 오류 메시지 노출)
check_expiration_dateinteger-0체크 유효 만료 일시 (Unix Timestamp)
  • 0: 제한 없음
resultinteger-처리 결과
  • 0: 비정상 (옵션 무시)
  • 1: 정상

kind2 응답 예시

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

kind3 응답 필드

data 항목

필드타입필수기본값설명
kindinteger-DRM 다운로드 콜백 유형
  • 3: 오프라인 재생 권한 확인
session_keystring--만료 일시 갱신용 세션 키 (content_expire_reset 요청 시 정합성 검증에 사용)
media_content_keystring-미디어 콘텐츠 키
start_atinteger-요청 시 수신한 start_at 값을 그대로 반환
content_expiredinteger-0콘텐츠 강제 만료 처리
  • 0: 재생 가능
  • 1: 재생 차단
content_deleteinteger-0다운로드 완료 직후 파일 삭제 여부
  • 0: 삭제하지 않음
  • 1: 삭제
content_expire_resetinteger-0기존 만료 정책 초기화
  • 0: 초기화하지 않음
  • 1: 초기화
expiration_dateinteger--재생 만료 일시 (Unix Timestamp)
expiration_countinteger--재생 허용 횟수 (예: 10 → 10회 재생 가능)
expiration_playtimeinteger--재생 제한 시간 (예: 60 → 60초, 3600 → 1시간 재생 가능)
resultinteger-처리 결과
  • 0: 비정상 (옵션 무시)
  • 1: 정상
messagestring--재생 차단(result: 0 또는 content_delete: 1 또는 content_expired: 1) 시 플레이어에 노출할 안내 메시지 (미입력 시 Kollus 기본 오류 메시지 노출)
check_abuseinteger-0오프라인 재생 시 kind3 호출 여부
  • 0: 호출하지 않음
  • 1: 항상 호출
check_expiration_dateinteger-0체크 유효 만료 일시 (Unix Timestamp)
  • 0: 제한 없음
⚠️초기화 옵션
  • 초기화 가능 옵션 (content_expire_reset: 1 설정 시)
    • expiration_date(재생 만료 일시), expiration_count(재생 허용 횟수), expiration_playtime(재생 제한 시간), check_expiration_date(체크 유효 만료 일시)
  • 초기화 무시 조건: content_expired 값이 1인 경우 content_expire_reset1로 설정하더라도 초기화 로직은 무시되며 재생이 차단됩니다.

kind3 응답 예시

{
"data": [
{
"kind": 3,
"session_key": "{SESSION_KEY}",
"media_content_key": "{MEDIA_CONTENT_KEY}",
"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
  • 예시: samsung/SM-G991N, google/Pixel_6

iOS

iOS 앱에서는 Apple에서 제공하는 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 인코딩 예제

Kollus의 응답 규격에 맞춰 헤더와 페이로드를 생성하고 HS256 알고리즘으로 서명하는 컴포넌트입니다.

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

/**
* Custom JWT Encoder for Kollus
*/
public class KollusJwtEncoder {

private final ObjectMapper objectMapper = new ObjectMapper();

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

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

// 2. Base64Url encode (without padding)
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;

// 3. Generate HS256 signature
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 다운로드 콜백 예제

Kollus 서버로부터의 요청(items)을 수신하여 유형별 비즈니스 로직을 수행하는 예제입니다.

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 = "{CUSTOM_KEY}";
private static final String JWT_KEY = "{SECURITY_KEY}";

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

/**
* Endpoint to receive DRM callbacks
*/
@PostMapping("/drm/callback")
public ResponseEntity<String> drmCallback(@RequestParam("items") String items) {
try {
// Parse the 'items' parameter
List<Map<String, Object>> itemsList = objectMapper.readValue(items, List.class);
List<Map<String, Object>> resultData = new ArrayList<>();

// Process each item in the list
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");

// Logic branching based on callback type (kind)
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);
}

// Generate the JWT response
Map<String, Object> payload = new HashMap<>();
payload.put("data", resultData);
String jwtToken = jwtEncoder.encode(payload, JWT_KEY);

// Configure response headers (Required: X-Kollus-UserKey)
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 (Download approval)
*/
private Map<String, Object> processKind1(String mediaContentKey, String clientUserId) {
// TODO: Verify user and content permissions in the database

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 day
response.put("expiration_count", 3);
response.put("expiration_playtime", 1800); // 30 minutes
response.put("vmcheck", 1);
response.put("check_abuse", 0);

return response;
}

/**
* kind2 (Download completion notification)
*/
private Map<String, Object> processKind2(String mediaContentKey, String clientUserId) {
// TODO: Record download completion status in the database

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 (Offline playback authorization)
*/
private Map<String, Object> processKind3(String mediaContentKey, String clientUserId,
Map<String, Object> item) {
// TODO: Verify playback permissions and expiration status in the database

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;
}

/**
* Create error 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);
}
}

예제 코드