DRM 다운로드 콜백
Kollus VOD는 콘텐츠의 업로드부터 재생까지 전체 워크플로우에 대해 콜백(Callback) 연동을 지원합니다.
고객사는 콜백 기능을 사용하여 주요 이벤트 발생 시 지정된 URL로 실시간 HTTP 알림을 수신할 수 있으며, 이를 통해 외부 시스템과의 데이터를 동기화하거나 업무 프로세스를 자동화할 수 있습니다.
DRM 다운로드 콜백(DRM Callback)은 최종 사용자(End user)가 Kollus 플레이어에서 콘텐츠 다운로드를 요청하거나, 이미 다운로드된 콘텐츠를 오프라인 상태에서 재생할 때 호출됩니다. 이 콜백을 통해 다운로드 권한 및 DRM 정책(만료 시각 등)을 서버에서 동적으로 제어할 수 있습니다.
유의 사항
- 데이터 규격 준수: 응답 데이터의 타입이나 값이 지정된 범위를 벗어날 경우, 콘텐츠 다운로드 및 재생이 즉시 차단됩니다.
- 설정 되돌리기 불가: 잘못된 설정값 전송으로 인해 발생한 사용 제한(만료 처리 등)은 이전 상태로 되돌릴 수 없으므로 정확한 값을 전송해야 합니다.
- 서버 가용성: 고객사 콜백 서버가 응답하지 않거나 응답 데이터가 형식에 맞지 않는 경우, 이후의 콜백 프로레스가 정상적으로 진행되지 않습니다.
콜백 설정 방법
콜백 URL은 Kollus VOD 콘솔에서 설정할 수 있습니다.
상세한 설정 위치는 콜백 설정 - 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
| 파라미터 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
kind | integer | ◯ | DRM 다운로드 콜백 유형
|
client_user_id | string | ◯ | 시청자 ID(JWT 생성 시 입력한 client_user_id와 동일) |
player_id | string | ◯ | 기기 고유 식별자 |
hardware_id | string | - | 기기 시리얼 넘버(PC에서만 제공) |
device_name | string | - | 기기 모델명 |
media_content_key | string | ◯ | 채널 내 콘텐츠의 고유 식별자 |
uservalues | JSON string | - | 고객사 정의 값(uservalue0~uservalue9) |
localtime | integer | - | 기기 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
| 파라미터 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
kind | integer | ◯ | DRM 다운로드 콜백 유형
|
session_key | string | ◯ | 만료 시간 연장 시 검증용 세션 키(content_expire_reset 요청 시, 동일한 session_key가 사용되었는지 확인함) |
client_user_id | string | ◯ | 시청자 ID(JWT 생성 시 입력한 client_user_id와 동일) |
player_id | string | ◯ | 기기 고유 식별자 |
hardware_id | string | - | 기기 시리얼 넘버(PC에서만 제공) |
device_name | string | - | 기기 모델명 |
media_content_key | string | ◯ | 채널 내 콘텐츠의 고유 식별자 |
start_at | integer | ◯ | 전송 요청 시각(Unix Timestamp → localtime) |
uservalues | JSON string | - | 고객사 정의 값(uservalue0~uservalue9) |
content_expired | integer | - | 재생 만료 여부
|
check_expired | integer | - | 체크 유효 시각 만료 여부
|
reset_req | integer | - | 일괄 업데이트 요청 여부
|
expiration_date | integer | - | 재생 만료 시각(Unix Timestamp) |
localtime | integer | - | 기기 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 응답 필드
| 필드 | 타입 | 필수 여부 | 기본값 | 설명 |
|---|---|---|---|---|
kind | integer | ◯ | - | DRM 다운로드 콜백 유형
|
media_content_key | string | ◯ | - | 채널 내 콘텐츠의 고유 식별자 |
expiration_date | integer | - | - | 재생 만료 시각(Unix Timestamp) |
expiration_count | integer | - | - | 재생 횟수 제한 예: 10 → 10회 재생 가능 |
expiration_playtime | integer | - | - | 재생 시간 제한(단위: 초) 예: 60 → 60초 재생 가능 |
expiration_playtime_type | integer | - | - |
|
result | integer | ◯ | - | 승인 결과
|
message | string | - | - | result가 0일 때 표시할 메시지미입력 시 Kollus 기본 오류 메시지가 표시됩니다. |
expiration_refresh_popup | integer | - | 0 | 만료 시 갱신 알림 표시 여부
|
vmcheck | integer | - | 1 | 가상머신 체크 여부(Html5 Player for PC에서만 사용 가능)
|
check_abuse | integer | - | 0 | 다운로드된 콘텐츠(오프라인) 재생 시 kind3 항상 호출 여부
|
offline_bookmark.download | integer | - | 0 | 오프라인 상태에서 북마크 다운로드 사용 여부 콘텐츠 다운로드 시점의 데이터만 다운로드되며, 이후 서버와 동기화되지 않습니다.
|
offline_bookmark.readonly | integer | - | 0 | 오프라인 상태에서 북마크 추가/삭제 사용 여부
|
kind1 응답 예시
{
"data" : [
{
"kind": 1,
"media_content_key": "XXXXXX",
"expiration_date": 1402444800,
"expiration_playtime": 1800,
"result": 1
}
]
}
kind2 응답 필드
| 필드 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
kind | integer | ◯ | - | DRM 다운로드 콜백 유형
|
media_content_key | string | ◯ | - | 채널 내 콘텐츠의 고유 식별자 |
content_delete | integer | - | 0 | 다운로드된 콘텐츠 즉시 삭제 여부
|
message | string | - | - | result가 0이거나 content_delete가 1일 때 표시할 메시지미입력 시 Kollus 기본 오류 메시지가 표시됩니다. |
check_expiration_date | integer | - | 0 | 체크 유효 만료 시각(Unix Timestamp)
|
result | integer | ◯ | - | 처리 결과
|
kind2 응답 예시
{
"data" : [
{
"kind": 2,
"media_content_key": "XXXXXX",
"content_delete": 1,
"result": 1
}
]
}
kind3 응답 필드
| 필드 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
kind | integer | ◯ | - | 3 |
session_key | string | - | - | DRM 다운로드 콜백 유형
|
media_content_key | string | ◯ | - | 채널 내 콘텐츠의 고유 식별자 |
start_at | integer | ◯ | - | 요청에 포함된 start_at 값을 그대로 반환 |
content_expired | integer | - | 0 | 다운로드된 콘텐츠 강제 만료 처리content_expire_reset 옵션을 통해 되돌릴 수 있습니다.
|
content_delete | integer | - | 0 | 다운로드된 콘텐츠 삭제 여부content_expired 값과 관계없이, 이 옵션이 설정되어 있으면 다운로드 콘텐츠는 삭제됩니다.
|
content_expire_reset | integer | - | 0 | 만료된 권한 초기화
|
expiration_date | integer | - | - | 재생 만료 시각(Unix Timestamp)content_expire_reset이 1인 경우 재설정됩니다. |
expiration_count | integer | - | - | 재생 횟수 제한content_expire_reset이 1인 경우 재설정됩니다.예: 10 → 10회 재생 가능 |
expiration_playtime | integer | - | - | 재생 시간 제한(단위: 초)content_expire_reset이 1인 경우 재설정됩니다.예: 3600 → 1시간 재생 가능 |
result | integer | ◯ | - | 처리 결과
|
message | string | - | - | result가 0이거나 content_delete 또는 content_expired가 1일 때 표시할 메시지미입력 시 Kollus 기본 오류 메시지가 표시됩니다. |
check_abuse | integer | - | 0 | 다운로드된 콘텐츠(오프라인) 재생 시 kind3 항상 호출 여부
|
check_expiration_date | integer | - | 0 | 체크 유효 만료 시각(Unix Timestamp)content_expire_reset이 1인 경우 재설정됩니다.
|
content_expired가 1인 경우, content_expire_reset을 1로 설정해도 해당 옵션은 무시됩니다.
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.DEVICE와 Build.MODEL을 /(슬래시)로 조합한 문자열을 사용합니다.
Build.DEVICE/Build.MODEL
iOS
iOS 앱에서는 iOS에서 제공하는 device_name을 사용합니다.
iOS device_name
| 디바이스 | device_name |
|---|---|
| iPhone1,1 | iPhone |
| iPhone1,2 | iPhone 3G |
| iPhone2,1 | iPhone 3GS |
| iPhone3,1 | iPhone 4 (GSM) |
| iPhone3,3 | iPhone 4 CDMA |
| iPhone4,1 | iPhone 4S |
| iPhone5,1 | iPhone 5 A1428 |
| iPhone5,2 | iPhone 5 A1429 |
| iPhone5,3 | iPhone 5c A1456/A1532 |
| iPhone5,4 | iPhone 5c A1507/A1516/A1529 |
| iPhone6,1 | iPhone 5s A1433/A1453 |
| iPhone6,2 | iPhone 5s A1457/A1518/A1530 |
| iPhone7,1 | iPhone 6 Plus |
| iPhone7,2 | iPhone 6 |
| iPhone8,1 | iPhone 6s |
| iPhone8,2 | iPhone 6s Plus |
| iPhone8,4 | iPhone SE |
| iPhone9,1 | iPhone 7 A1660/A1779/A1780 |
| iPhone9,2 | iPhone 7 Plus A1661/A1785/A1786 |
| iPhone9,3 | iPhone 7 A1778 |
| iPhone9,4 | iPhone 7 Plus A1784 |
| iPhone10,1 | iPhone 8 A1863/A1906 |
| iPhone10,2 | iPhone 8 Plus A1864/A1898 |
| iPhone10,3 | iPhone X A1865/A1902 |
| iPhone10,4 | iPhone 8 A1905 |
| iPhone10,5 | iPhone 8 Plus A1897 |
| iPhone10,6 | iPhone X A1901 |
| iPad1,1 | iPad |
| iPad2,1 | iPad 2 WiFi |
| iPad2,2 | iPad 2 (GSM) |
| iPad2,3 | iPad 2 CDMA |
| iPad2,4 | iPad 2 WiFi (revised) |
| iPad2,5 | iPad mini WiFi |
| iPad2,6 | iPad mini A1454 |
| iPad2,7 | iPad mini A1455 |
| iPad3,1 | iPad 3rd gen (WiFi) |
| iPad3,2 | iPad 3rd gen (WiFi+LTE Verizon) |
| iPad3,3 | iPad 3rd gen (WiFi+LTE AT&T) |
| iPad3,4 | iPad 4th gen (WiFi) |
| iPad3,5 | iPad 4th gen A1459 |
| iPad3,6 | iPad 4th gen A1460 |
| iPad4,1 | iPad Air WiFi |
| iPad4,2 | iPad Air WiFi+LTE |
| iPad4,3 | iPad Air Rev |
| iPad4,4 | iPad mini 2 WiFi |
| iPad4,5 | iPad mini 2 WiFi+LTE |
| iPad4,6 | iPad mini 2 Rev |
| iPad4,7 | iPad mini 3 WiFi |
| iPad4,8 | iPad mini 3 A1600 |
| iPad4,9 | iPad mini 3 A1601 |
| iPad5,1 | iPad mini 4 WiFi |
| iPad5,2 | iPad mini 4 WiFi+LTE |
| iPad5,3 | iPad Air 2 WiFi |
| iPad5,4 | iPad Air 2 WiFi+LTE |
| iPad6,3 | iPad Pro 9.7 inch WiFi |
| iPad6,4 | iPad Pro 9.7 inch WiFi+LTE |
| iPad6,7 | iPad Pro 12.9 inch WiFi |
| iPad6,8 | iPad Pro 12.9 inch WiFi+LTE |
| iPad6,11 | iPad 9.7 Inch 5th Gen WiFi Only |
| iPad6,12 | iPad 9.7 Inch 5th Gen WiFi/Cellular |
| iPad7,1 | iPad Pro 12.9 inch A1670 |
| iPad7,2 | iPad Pro 12.9 inch A18219 |
| iPad7,3 | iPad Pro 10.5 inch A1701 |
| iPad7,4 | iPad Pro 10.5 inch A1709 |
| iPad7,5 | iPad 6th gen A1893 |
| iPad7,6 | iPad 6th gen A1954 |
| iPod1,1 | iPod touch |
| iPod2,1 | iPod touch 2nd gen |
| iPod3,1 | iPod touch 3rd gen |
| iPod4,1 | iPod touch 4th gen |
| iPod5,1 | iPod touch 5th gen |
| iPod7,1 | iPod 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);
}
}