57 changed files with 2705 additions and 95 deletions
@ -0,0 +1,15 @@ |
|||||||
|
root = true |
||||||
|
|
||||||
|
[*] |
||||||
|
charset = utf-8 |
||||||
|
end_of_line = lf |
||||||
|
insert_final_newline = true |
||||||
|
indent_style = space |
||||||
|
indent_size = 4 |
||||||
|
trim_trailing_whitespace = true |
||||||
|
|
||||||
|
[*.md] |
||||||
|
trim_trailing_whitespace = false |
||||||
|
|
||||||
|
[*.{yml,yaml}] |
||||||
|
indent_size = 2 |
||||||
@ -0,0 +1,69 @@ |
|||||||
|
## 프로젝트 구조 |
||||||
|
|
||||||
|
- domain: 순수한 도메인 관련 모델/기능 |
||||||
|
- dsl: TC DSL 관련 정의와 기능들 |
||||||
|
- proxy: 프록시 푸쉬 서버 관련 기능들 |
||||||
|
- STB 에 system command 와 cue 송신 |
||||||
|
- STB 의 auth, ads, vast, assetConvert 관련 HTTP 요청을 수신 |
||||||
|
- QA 들이 apache 를 사용하여 하던 행위를 대체 |
||||||
|
- TC 에서 HTTP 응답 변환이나 일시 정지, 딜레이가 필요할 경우 수행 |
||||||
|
- tc: TC 테스터와 TC 정의들 |
||||||
|
- 실제 TC 들을 정의 |
||||||
|
- 정의된 TC 들을 수행 |
||||||
|
|
||||||
|
## 실행 방법 |
||||||
|
|
||||||
|
1. STB 대여 (아래 사항들 확인 필요함) |
||||||
|
- STB 가 접속되는 AP 이름과 비밀번호 |
||||||
|
- STB IP, adb 포트 번호 |
||||||
|
1. STB 설치 |
||||||
|
1. 컴퓨터의 USB 에 캡쳐보드를 꼽고 캡쳐보드와 STB output 과 연결 |
||||||
|
1. 컴퓨터에 obs-studio 설치 및 실행 |
||||||
|
2. 왼쪽 하단의 Sources 의 + 버튼(add sources) 클릭 |
||||||
|
|
||||||
|
 |
||||||
|
3. video capture device(V4L2) (리눅스 기준) 클릭 |
||||||
|
4. 디바이스를 바꿔가면서 STB 를 찾아 선택 후 OK 클릭 |
||||||
|
|
||||||
|
 |
||||||
|
|
||||||
|
1. application.yml 에서 anypoint.android-qa.stb 하위 정보들을 STB 의 정보로 수정 |
||||||
|
1. obs-studio 종료 후 android-qa 애플리케이션 실행 (obs-studio 종료해야 정상 작동됨) |
||||||
|
|
||||||
|
## TC 수행 순서 |
||||||
|
|
||||||
|
1. adb 를 사용하여 STB 이 프록시 서버를 바라보도록 변경 |
||||||
|
```shell |
||||||
|
adb shell am broadcast -a tv.anypoint.agent.app.CHANGE_TEST_PROPERTY --es change.command CHANGE_API_ENDPOINT --es api.endpoint http://<PROXY_SERVER_URL>:<PROXY_SERVER_PORT> |
||||||
|
``` |
||||||
|
1. |
||||||
|
|
||||||
|
## ffmpeg 리눅스에서 사용 방법 |
||||||
|
|
||||||
|
v4l2(video4linux2) 를 사용하여 캡쳐보드의 devicePath 구하기 |
||||||
|
|
||||||
|
```shell |
||||||
|
$ v4l2-ctl --list-devices |
||||||
|
Integrated Camera: Integrated C (usb-0000:00:14.0-8): |
||||||
|
/dev/video0 |
||||||
|
/dev/video1 |
||||||
|
/dev/video2 |
||||||
|
/dev/video3 |
||||||
|
/dev/media0 |
||||||
|
/dev/media1 |
||||||
|
|
||||||
|
USB Video: USB Video (usb-0000:09:00.0-2.4): |
||||||
|
/dev/video5 |
||||||
|
/dev/video6 |
||||||
|
/dev/media3 |
||||||
|
``` |
||||||
|
|
||||||
|
- 비디오: -f video4linux2 -i `<VIDEO_PATH>` 의 형식으로 입력. |
||||||
|
- 오디오: -f alsa -ac 2 -i default 만 넣어도 되는데 캡쳐보드 전체가 동일한 지는 확인 안해 봄 |
||||||
|
|
||||||
|
아래 예제 참고 |
||||||
|
|
||||||
|
```shell |
||||||
|
$ ffmpeg -f video4linux2 -i /dev/video5 -f alsa -ac 2 -i default -b:v 10M -vcodec h264_nvenc -pixel_format yuv420p -rtbufsize 1000M output_new_captureboard_30fps.mp4 |
||||||
|
``` |
||||||
|
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 202 KiB |
@ -0,0 +1,6 @@ |
|||||||
|
package tv.anypoint.domain.adb |
||||||
|
|
||||||
|
enum class ChangeCommand { |
||||||
|
CHANGE_API_ENDPOINT, |
||||||
|
CHANGE_TEST_FLAG |
||||||
|
} |
||||||
@ -1,4 +1,4 @@ |
|||||||
package tv.anypoint.dsl.model.adb |
package tv.anypoint.domain.adb |
||||||
|
|
||||||
enum class ExtraKey(val string: String) { |
enum class ExtraKey(val string: String) { |
||||||
EXTRA_JSON("EXTRA_JSON"), |
EXTRA_JSON("EXTRA_JSON"), |
||||||
@ -1,6 +1,5 @@ |
|||||||
package tv.anypoint.dsl.model.adb |
package tv.anypoint.domain.adb |
||||||
|
|
||||||
enum class IntentAction(val string: String) { |
enum class IntentAction(val string: String) { |
||||||
CHANGE_TEST_PROPERTY("tv.anypoint.agent.app.CHANGE_TEST_PROPERTY") |
CHANGE_TEST_PROPERTY("tv.anypoint.agent.app.CHANGE_TEST_PROPERTY") |
||||||
; |
|
||||||
} |
} |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
package tv.anypoint.domain.agent |
||||||
|
|
||||||
|
data class AuthRequest( |
||||||
|
var fingerPrint: String = "", |
||||||
|
val soId: Int = 0, |
||||||
|
val uuid: String = "", |
||||||
|
val usePersonalizedAd: Boolean = true, |
||||||
|
val test: Boolean = false, |
||||||
|
val zipCode: String = "", |
||||||
|
val freeStorage: Long = 0, |
||||||
|
val usedStorage: Long = 0, |
||||||
|
val cachedStorage: Long = 0, |
||||||
|
var modelName: String? = null, |
||||||
|
val firmwareBuildDate: String? = null, |
||||||
|
val firmwareVer: String? = null, |
||||||
|
val fullFirmwareVer: String? = null, |
||||||
|
val appVersion: String? = null, |
||||||
|
val platformAdId: String? = null, |
||||||
|
val sdkVersion: String? = null, |
||||||
|
) |
||||||
@ -0,0 +1,112 @@ |
|||||||
|
package tv.anypoint.domain.agent |
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit |
||||||
|
|
||||||
|
data class AuthResponse( |
||||||
|
var deviceId: Long = 0, |
||||||
|
var deviceTypeId: Int = 0, |
||||||
|
var uuid: String = "", |
||||||
|
var monitoringInterval: Long = 0, |
||||||
|
val endpoints: Endpoints = Endpoints(), |
||||||
|
var channels: List<ProgramProviderChannel> = emptyList(), |
||||||
|
val adConfig: AdConfig = AdConfig(), |
||||||
|
var chViewMinTime: Int = 0, |
||||||
|
var kidWatermark: KidWatermark? = null, |
||||||
|
val accessToken: String? = null, |
||||||
|
val pushSecretKey: String = "Tx?c)T!3e.52Th5J", |
||||||
|
val storeAppId: String? = null, |
||||||
|
) { |
||||||
|
|
||||||
|
companion object { |
||||||
|
val DEFAULT_MONITOR_PERIOD = TimeUnit.MINUTES.toMillis(3) |
||||||
|
private val MINIMUM_MONITOR_INTERVAL = TimeUnit.SECONDS.toMillis(30) |
||||||
|
} |
||||||
|
|
||||||
|
fun authorization() = "Anypoint ${accessToken ?: ""}" |
||||||
|
|
||||||
|
fun monitoringInterval() = monitoringInterval.let { |
||||||
|
if (it < MINIMUM_MONITOR_INTERVAL) DEFAULT_MONITOR_PERIOD else it |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
data class Endpoints( |
||||||
|
var auth: String = "", |
||||||
|
var requestAds: String = "", |
||||||
|
var adSyncResult: String = "", |
||||||
|
var appLog: String = "", |
||||||
|
var event: String = "", |
||||||
|
var pushServers: List<String> = emptyList(), |
||||||
|
var stateLog: String = "", |
||||||
|
var impressionLog: String = "", |
||||||
|
var ntpServers: List<String> = emptyList(), |
||||||
|
var assetRequest: String = "", |
||||||
|
var proxyAdLog: String = "", |
||||||
|
var externalDeviceMessageHost: String = "", |
||||||
|
var externalDeviceMessagePort: Int = 0 |
||||||
|
) |
||||||
|
|
||||||
|
enum class VideoPlayMode { |
||||||
|
SERIALIZED, PARALLEL |
||||||
|
} |
||||||
|
|
||||||
|
enum class VideoMediaType { |
||||||
|
CONCATENATING, LEGACY |
||||||
|
} |
||||||
|
|
||||||
|
// WARNING: local db(data class Device)의 Embedded 엔티티이므로 프로퍼티 변경 시, db 버전 올려야 함 |
||||||
|
data class AdConfig( |
||||||
|
var maxDownloadBandwidth: Long = 150 * 1024, |
||||||
|
var maxLazyDownloadBandwidth: Long = 1024 * 1024, |
||||||
|
var appPath: String = "", |
||||||
|
var maxUsableStorage: Long = 0, |
||||||
|
var minFreeStorage: Long = 100 * 1024 * 1024, |
||||||
|
var trackingRetryInterval: Int = 0, |
||||||
|
var trackingRetryCount: Int = 0, |
||||||
|
/** |
||||||
|
* 남은 광고 시간 허용 기준 (해당 값 이하인 경우는 마지막 프레임을 유지하는 형태) 단위: 초 |
||||||
|
*/ |
||||||
|
var remnantTimeThreshold: Int = 2_000, |
||||||
|
var maxEndAdPlaytime: Int = 15_000, |
||||||
|
var transitionDelay: Int = 0, |
||||||
|
var videoPlayMode: VideoPlayMode = VideoPlayMode.PARALLEL, |
||||||
|
var videoMediaType: VideoMediaType = VideoMediaType.CONCATENATING, |
||||||
|
var startDelay: Int = 0, |
||||||
|
var stopDelay: Int = 0, |
||||||
|
var startRenderDelay: Int = 0, |
||||||
|
var stopRenderDelay: Int = 0, |
||||||
|
var overPlayTimeThreshold: Int = 0, |
||||||
|
// (플레이 리스트의 1번째 소재가 플레이 완료되기 전 x초) |
||||||
|
// 해당 시간 전까지 준비 완료가 안되면 대체 소재로 플레이 |
||||||
|
// sdk 사용시 player의 버퍼 시간보다 1초 이상 커야 한다 |
||||||
|
// (sdk 내장 플레이어는 현재 3초) |
||||||
|
var continuousCueMarginTime: Int = 34, // 연속 큐로 간주하기 위한 최대 cue gap |
||||||
|
var minAdPlayAvailableMemMB: Int = 0, |
||||||
|
var useSdkDigitalCue: Boolean = true, |
||||||
|
var minCueGap: Int = TimeUnit.SECONDS.toMillis(5).toInt(), |
||||||
|
var maxCueOutDelay: Int = TimeUnit.SECONDS.toMillis(15).toInt(), |
||||||
|
var maxCueDuration: Int = TimeUnit.MINUTES.toMillis(10).toInt(), |
||||||
|
var playerStateCheckParam: PlayerStateCheckParam = PlayerStateCheckParam(), |
||||||
|
var useLastPositionGoogleAd: Boolean = false, |
||||||
|
var retainedChannelStream: Boolean = true, |
||||||
|
val maxAssignLoop: Int = 20, |
||||||
|
val onCueCachingTimeWeight: Float = 0.5f, |
||||||
|
var allowedUnfilledPlaylist: Boolean = false, |
||||||
|
var allowedFirstUnreadyAd: Boolean = false, |
||||||
|
var onceAdReadyResponse: Boolean = true, |
||||||
|
var onceAdReadyRequiredTime: Int = 1000, |
||||||
|
var minRequestAdDuration: Int = 5_000, |
||||||
|
var sendBeaconFireResult: Boolean = false, |
||||||
|
var onPlayDelay: Int = 0, |
||||||
|
var skipOffset: Int = 5_000, |
||||||
|
val useMultipleAssetConvert: Boolean = false, |
||||||
|
) |
||||||
|
|
||||||
|
// WARNING: local db(data class Device)의 Embedded 엔티티이므로 프로퍼티 변경 시, db 버전 올려야 함 |
||||||
|
data class KidWatermark( |
||||||
|
var imageUrl: String = "", |
||||||
|
var crc: Long = 0, |
||||||
|
var left: Int = 0, |
||||||
|
var top: Int = 0, |
||||||
|
var width: Int = 0, |
||||||
|
var height: Int = 0 |
||||||
|
) |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
package tv.anypoint.domain.agent |
||||||
|
|
||||||
|
data class Cue( |
||||||
|
var id: Long = 0, |
||||||
|
var ppId: Int = 0, |
||||||
|
/** 큐 시작 시각 (ms) */ |
||||||
|
var time: Long = 0, |
||||||
|
/** 큐 길이 (ms) */ |
||||||
|
var duration: Int = 0, |
||||||
|
var type: CueType = CueType.NORMAL, |
||||||
|
var placementId: Int = 0, |
||||||
|
var startPts: Long = 0, |
||||||
|
var multicastUri: String = "", |
||||||
|
var isPushCue: Boolean = false, |
||||||
|
var owner: CueOwner = CueOwner.OTT |
||||||
|
) { |
||||||
|
/** 큐 종료 시각 (ms) = [time] + [duration] */ |
||||||
|
val endTime: Long |
||||||
|
get() = time + duration |
||||||
|
} |
||||||
|
|
||||||
|
enum class CueType(val v: Byte) { |
||||||
|
INVALID_CUE_TYPE(Byte.MIN_VALUE), |
||||||
|
NORMAL(0), |
||||||
|
CANCEL(1), |
||||||
|
REPLACE(2) |
||||||
|
; |
||||||
|
|
||||||
|
companion object { |
||||||
|
operator fun get(v: Byte): CueType { |
||||||
|
return values().find { it.v == v } ?: INVALID_CUE_TYPE |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
package tv.anypoint.domain.agent |
||||||
|
|
||||||
|
enum class CueOwner { |
||||||
|
PP, |
||||||
|
SO, |
||||||
|
OTT |
||||||
|
; |
||||||
|
|
||||||
|
companion object { |
||||||
|
fun fromCode(code: Int): CueOwner { |
||||||
|
return CueOwner.values().last { it.ordinal == code } |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
package tv.anypoint.domain.agent |
||||||
|
|
||||||
|
enum class PlayType { |
||||||
|
DNP, |
||||||
|
LAZY_DNP, |
||||||
|
STREAMING |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
package tv.anypoint.domain.agent |
||||||
|
|
||||||
|
data class PlayerStateCheckParam( |
||||||
|
val playerStateCheckDuration: Int = 1500, |
||||||
|
val maxPlayerInvalidateInterval: Int = 300 |
||||||
|
) |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
package tv.anypoint.domain.agent |
||||||
|
|
||||||
|
data class ProgramProviderChannel( |
||||||
|
val id: Int, |
||||||
|
val delay: Int = 0, |
||||||
|
val scte35Delay: Int = 0, |
||||||
|
val serviceId: String, |
||||||
|
val kid: Boolean = false, |
||||||
|
/** 없을 경우 service 채널 아님 */ |
||||||
|
val placementIds: IntArray? = null, |
||||||
|
val testPlacementIds: IntArray? = null, |
||||||
|
val taxonomies: List<String>? = null, |
||||||
|
) { |
||||||
|
var recordId: Int = 0 |
||||||
|
override fun equals(other: Any?): Boolean { |
||||||
|
if (this === other) return true |
||||||
|
if (javaClass != other?.javaClass) return false |
||||||
|
other as ProgramProviderChannel |
||||||
|
if (id != other.id) return false |
||||||
|
if (delay != other.delay) return false |
||||||
|
if (serviceId != other.serviceId) return false |
||||||
|
if (kid != other.kid) return false |
||||||
|
if (placementIds != null) { |
||||||
|
if (other.placementIds == null) return false |
||||||
|
if (!placementIds.contentEquals(other.placementIds)) return false |
||||||
|
} else if (other.placementIds != null) return false |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
override fun hashCode(): Int { |
||||||
|
var result = id |
||||||
|
result = 31 * result + delay |
||||||
|
result = 31 * result + serviceId.hashCode() |
||||||
|
result = 31 * result + kid.hashCode() |
||||||
|
result = 31 * result + (placementIds?.contentHashCode() ?: 0) |
||||||
|
return result |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
package tv.anypoint.domain.agent |
||||||
|
|
||||||
|
data class StateChangeLog( |
||||||
|
val soId: Int = 0, |
||||||
|
val deviceId: Long = 0, |
||||||
|
val previousState: State? = null, |
||||||
|
var currentState: State? = null |
||||||
|
) |
||||||
|
|
||||||
|
data class State( |
||||||
|
val inTime: Long, |
||||||
|
val outTime: Long? = null, |
||||||
|
var state: TvState = TvState.ETC, |
||||||
|
var appId: String? = null, |
||||||
|
val channelServiceId: String? = null, |
||||||
|
val vodTitle: String? = null, |
||||||
|
var isCompleted: Boolean = false |
||||||
|
) |
||||||
|
|
||||||
|
enum class TvState { |
||||||
|
HOME, LINEAR_TV, VOD, APP, SLEEP, ETC |
||||||
|
} |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
package tv.anypoint.domain.agent.ad |
||||||
|
|
||||||
|
data class Ad( |
||||||
|
val id: Long, |
||||||
|
val adRequest: AdRequest? = null, |
||||||
|
var asset: Asset? = null, |
||||||
|
val campaignId: Long = 0, |
||||||
|
val campaignType: CampaignType = CampaignType.CHARGE, |
||||||
|
val viewSequence: Int = 0, |
||||||
|
var paused: Boolean = false, |
||||||
|
var price: Float? = null, |
||||||
|
var priceModel: String? = null, |
||||||
|
var priceCurrency: String? = null, |
||||||
|
val startAt: Long = 0, |
||||||
|
val endAt: Long = 0, |
||||||
|
val extraPlay: Int = 0, |
||||||
|
val extraPlayContinuous: Boolean = false, |
||||||
|
val click: Click? = null, |
||||||
|
val exclusiveAdIds: Set<Long>? = null |
||||||
|
) |
||||||
|
|
||||||
|
enum class CampaignType { |
||||||
|
CHARGE, |
||||||
|
FREE, |
||||||
|
HOUSE, |
||||||
|
BUFFER, |
||||||
|
ENDING |
||||||
|
} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
package tv.anypoint.domain.agent.ad |
||||||
|
|
||||||
|
import tv.anypoint.domain.agent.PlayType |
||||||
|
|
||||||
|
|
||||||
|
enum class EncodingType { |
||||||
|
NONE, |
||||||
|
MANUAL, |
||||||
|
AUTO |
||||||
|
} |
||||||
|
|
||||||
|
enum class ExtAdProtocol { |
||||||
|
VAST, |
||||||
|
GOOGLE_AD |
||||||
|
} |
||||||
|
|
||||||
|
data class AdRequest( |
||||||
|
var url: String, |
||||||
|
val updateInterval: Int = 0, |
||||||
|
val timeout: Int = 1000, |
||||||
|
val encodingType: EncodingType = EncodingType.AUTO, |
||||||
|
val extPlatformMappingId: Long = 0, |
||||||
|
val onCue: Boolean = false, |
||||||
|
var protocol: ExtAdProtocol = ExtAdProtocol.VAST, |
||||||
|
val assetPlayType: PlayType = PlayType.DNP, // android room, field name duplication 방지 |
||||||
|
var maxDuration: Int = 15_000, |
||||||
|
var minDuration: Int = 3_000, |
||||||
|
val headers: Map<String, String>? = null, |
||||||
|
var fallbackUrl: String? = null, |
||||||
|
val fallbackProtocol: ExtAdProtocol = ExtAdProtocol.VAST, |
||||||
|
) |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
package tv.anypoint.domain.agent.ad |
||||||
|
|
||||||
|
data class AdsResponse( |
||||||
|
val status: AdListResponseStatus = AdListResponseStatus.OK, |
||||||
|
val ads: List<Ad> = listOf(), |
||||||
|
val targetAds: List<TargetAd> = listOf(), |
||||||
|
val deleteCache: Boolean = false, |
||||||
|
) |
||||||
|
|
||||||
|
enum class AdListResponseStatus { |
||||||
|
OK, |
||||||
|
ERROR, |
||||||
|
IGNORE_ERROR |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
package tv.anypoint.domain.agent.ad |
||||||
|
|
||||||
|
data class AdsSyncRequest( |
||||||
|
val deviceId: Long, |
||||||
|
val freeStorage: Long, |
||||||
|
val usedStorage: Long, |
||||||
|
val cachedStorage: Long, |
||||||
|
val adIds: List<Long> |
||||||
|
) |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
package tv.anypoint.domain.agent.ad |
||||||
|
|
||||||
|
import tv.anypoint.domain.agent.PlayType |
||||||
|
|
||||||
|
data class Asset( |
||||||
|
var assetId: Long = 0L, |
||||||
|
var crc: String = "0", |
||||||
|
var mediaUrl: String = "", |
||||||
|
/** In milliseconds */ |
||||||
|
var duration: Int = 0, |
||||||
|
var bytes: Long = 0, |
||||||
|
var playType: PlayType = PlayType.DNP, |
||||||
|
var extendedQueryString: String = "" |
||||||
|
) |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
package tv.anypoint.domain.agent.ad |
||||||
|
|
||||||
|
data class AssetConvertResponse(val id: Long) |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
package tv.anypoint.domain.agent.ad |
||||||
|
|
||||||
|
data class Click( |
||||||
|
val targetType: String, |
||||||
|
val targetValue: String, |
||||||
|
val startDelay: Int = 0, |
||||||
|
val endDelay: Int = 0 |
||||||
|
) |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
package tv.anypoint.domain.agent.ad |
||||||
|
|
||||||
|
data class ProgramPlacement( |
||||||
|
val id: Int, |
||||||
|
val placementUniqueKey: PlacementUniqueKey, |
||||||
|
val durationInMs: Long, |
||||||
|
val mediaType: MediaType, |
||||||
|
val test: Boolean |
||||||
|
) |
||||||
|
|
||||||
|
data class PlacementUniqueKey( |
||||||
|
val mediaId: Int, |
||||||
|
val contentId: Int, |
||||||
|
val programId: Int, |
||||||
|
val platformId: Int, |
||||||
|
val contentVendorId: String? = null |
||||||
|
) |
||||||
|
|
||||||
|
enum class MediaType { |
||||||
|
SO, PP |
||||||
|
} |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
package tv.anypoint.domain.agent.ad |
||||||
|
|
||||||
|
|
||||||
|
data class TargetAd( |
||||||
|
val hours: String? = null, |
||||||
|
val days: String? = null, |
||||||
|
val adIds: String, |
||||||
|
var placements: String? = null |
||||||
|
) { |
||||||
|
companion object { |
||||||
|
fun from( |
||||||
|
hours: Set<Int> = (0..23).toSet(), |
||||||
|
days: Set<Int> = (1..7).toSet(), |
||||||
|
placements: Set<ProgramPlacement> = emptySet(), |
||||||
|
adIds: Set<Int> = emptySet() |
||||||
|
) = TargetAd( |
||||||
|
hours = hours.joinWithVerticalBar(), |
||||||
|
days = days.joinWithVerticalBar(), |
||||||
|
placements = placements |
||||||
|
.map { "${it.placementUniqueKey.contentId}-${it.placementUniqueKey.programId}" } |
||||||
|
.joinWithVerticalBar(), |
||||||
|
adIds = adIds.joinWithVerticalBar() |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun <T> Iterable<T>.joinWithVerticalBar() = this.joinToString(prefix = "|", postfix = "|", separator = "|") |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
package tv.anypoint.domain.agent.ad |
||||||
|
|
||||||
|
data class VastResponse(val id: Long) |
||||||
@ -1,6 +1,6 @@ |
|||||||
package tv.anypoint.dsl.handler |
package tv.anypoint.dsl.handler |
||||||
|
|
||||||
class HttpHandler<R> ( |
class HttpHandler<R>( |
||||||
var convert: (response: R) -> R = { it }, |
var convert: (response: R) -> R = { it }, |
||||||
var validate: (response: R) -> Boolean = { true } |
var validate: (response: R) -> Boolean = { true } |
||||||
) |
) |
||||||
@ -1,6 +0,0 @@ |
|||||||
package tv.anypoint.dsl.model.http |
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable |
|
||||||
|
|
||||||
@Serializable |
|
||||||
data class AdsResponse(val id: Long) |
|
||||||
@ -1,6 +0,0 @@ |
|||||||
package tv.anypoint.dsl.model.http |
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable |
|
||||||
|
|
||||||
@Serializable |
|
||||||
data class AssetConvertResponse(val id: Long) |
|
||||||
@ -1,6 +0,0 @@ |
|||||||
package tv.anypoint.dsl.model.http |
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable |
|
||||||
|
|
||||||
@Serializable |
|
||||||
data class AuthResponse(val id: Long) |
|
||||||
@ -1,6 +0,0 @@ |
|||||||
package tv.anypoint.dsl.model.http |
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable |
|
||||||
|
|
||||||
@Serializable |
|
||||||
data class VastResponse(val id: Long) |
|
||||||
@ -0,0 +1,19 @@ |
|||||||
|
package tv.anypoint.dsl.serialization |
||||||
|
|
||||||
|
import kotlinx.serialization.json.Json |
||||||
|
import kotlinx.serialization.modules.SerializersModule |
||||||
|
import org.springframework.context.annotation.Bean |
||||||
|
import org.springframework.context.annotation.Configuration |
||||||
|
import java.time.LocalDate |
||||||
|
import java.time.LocalDateTime |
||||||
|
|
||||||
|
@Configuration |
||||||
|
class JsonConfiguration { |
||||||
|
@Bean |
||||||
|
fun json(): Json = Json { |
||||||
|
serializersModule = SerializersModule { |
||||||
|
contextual(LocalDate::class, LocalDateSerializer()) |
||||||
|
contextual(LocalDateTime::class, LocalDateTimeSerializer()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
package tv.anypoint.dsl.serialization |
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi |
||||||
|
import kotlinx.serialization.KSerializer |
||||||
|
import kotlinx.serialization.Serializer |
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind |
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor |
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor |
||||||
|
import kotlinx.serialization.encoding.Decoder |
||||||
|
import kotlinx.serialization.encoding.Encoder |
||||||
|
import java.time.LocalDate |
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class) |
||||||
|
@Suppress("EXTERNAL_SERIALIZER_USELESS") |
||||||
|
@Serializer(forClass = LocalDate::class) |
||||||
|
class LocalDateSerializer : KSerializer<LocalDate> { |
||||||
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING) |
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: LocalDate) { |
||||||
|
encoder.encodeString(value.toString()) |
||||||
|
} |
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): LocalDate = LocalDate.parse(decoder.decodeString()) |
||||||
|
} |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
package tv.anypoint.dsl.serialization |
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi |
||||||
|
import kotlinx.serialization.KSerializer |
||||||
|
import kotlinx.serialization.Serializer |
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind |
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor |
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor |
||||||
|
import kotlinx.serialization.encoding.Decoder |
||||||
|
import kotlinx.serialization.encoding.Encoder |
||||||
|
import java.time.LocalDateTime |
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class) |
||||||
|
@Suppress("EXTERNAL_SERIALIZER_USELESS") |
||||||
|
@Serializer(forClass = LocalDateTime::class) |
||||||
|
class LocalDateTimeSerializer : KSerializer<LocalDateTime> { |
||||||
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING) |
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: LocalDateTime) { |
||||||
|
encoder.encodeString(value.toString()) |
||||||
|
} |
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): LocalDateTime = LocalDateTime.parse(decoder.decodeString()) |
||||||
|
} |
||||||
@ -0,0 +1,30 @@ |
|||||||
|
package tv.anypoint.proxy.adapter |
||||||
|
|
||||||
|
import org.springframework.cloud.openfeign.FeignClient |
||||||
|
import org.springframework.web.bind.annotation.PostMapping |
||||||
|
import org.springframework.web.bind.annotation.RequestBody |
||||||
|
import org.springframework.web.bind.annotation.RequestParam |
||||||
|
import tv.anypoint.domain.agent.AuthRequest |
||||||
|
import tv.anypoint.domain.agent.AuthResponse |
||||||
|
import tv.anypoint.domain.agent.ad.AdsResponse |
||||||
|
import tv.anypoint.domain.agent.ad.AdsSyncRequest |
||||||
|
|
||||||
|
|
||||||
|
@FeignClient( |
||||||
|
name = "DeviceV3Adapter", |
||||||
|
path = "/v3/device" |
||||||
|
) |
||||||
|
interface DeviceV3Adapter { |
||||||
|
@PostMapping("/auth") |
||||||
|
fun auth(@RequestBody body: AuthRequest): AuthResponse |
||||||
|
|
||||||
|
@PostMapping("/ads") |
||||||
|
fun ads( |
||||||
|
@RequestParam(required = true) deviceId: Long, |
||||||
|
@RequestParam("freeStorage", required = true, defaultValue = "0") freeStorage: Long = 0, |
||||||
|
@RequestParam("usedStorage", required = true, defaultValue = "0") usedStorage: Long = 0 |
||||||
|
): AdsResponse |
||||||
|
|
||||||
|
@PostMapping("/sync/ads") |
||||||
|
fun syncAds(@RequestBody request: AdsSyncRequest) |
||||||
|
} |
||||||
@ -0,0 +1,127 @@ |
|||||||
|
package tv.anypoint.proxy.service |
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest |
||||||
|
import kotlinx.coroutines.* |
||||||
|
import java.net.Inet4Address |
||||||
|
import java.net.InetAddress |
||||||
|
import java.net.NetworkInterface |
||||||
|
import java.util.* |
||||||
|
|
||||||
|
object NetworkUtil { |
||||||
|
fun getPrivateIpV4(setTopBoxAddress: InetAddress): String = getPrivateIpV4List() |
||||||
|
.chooseSimilarIp(setTopBoxAddress)?.hostAddress |
||||||
|
?: throw RuntimeException() |
||||||
|
|
||||||
|
/** |
||||||
|
* Gives the private IP of this device |
||||||
|
* @return The private IP |
||||||
|
*/ |
||||||
|
fun getPrivateIpV4List(): List<InetAddress> { |
||||||
|
val result = mutableListOf<InetAddress>() |
||||||
|
NetworkInterface.getNetworkInterfaces().forEach { n -> |
||||||
|
result += n.inetAddresses.filter { it is Inet4Address && it.isSiteLocalAddress } |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gives the list of all open IPs of the private network |
||||||
|
* in which this device is connected to. |
||||||
|
* |
||||||
|
* For the time saving, this method performs multithreaded |
||||||
|
* ping test for 5 seconds in each thread, excluding |
||||||
|
* 192.168.0.1 and this device's private IP. |
||||||
|
* |
||||||
|
* @return List of all open IPs as [InetAddress] |
||||||
|
*/ |
||||||
|
fun findAllOpenPrivateIps(skipSelf: Boolean = false) = runBlocking { |
||||||
|
val todos = mutableListOf<Deferred<Unit?>>() |
||||||
|
val privateIpList = getPrivateIpV4List() |
||||||
|
val available2dArray = Array(privateIpList.size) { arrayOfNulls<InetAddress>(256) } |
||||||
|
|
||||||
|
privateIpList.forEachIndexed { i, inetAddress -> |
||||||
|
todos += Inet4Iterator(inetAddress, skipSelf) |
||||||
|
.map { |
||||||
|
async(Dispatchers.IO) { |
||||||
|
if (it.second.isReachable(1000)) available2dArray[i][it.first] = it.second else null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
todos.awaitAll() |
||||||
|
return@runBlocking available2dArray.flatten().filterNotNull() |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The iterator that iterates all near IPs from the given [originalIp] |
||||||
|
* |
||||||
|
* If the given IP is a.b.c.d, then it will iterate from |
||||||
|
* a.b.c.2 to a.b.c.255, when start=2 and [end]=255 (default) |
||||||
|
*/ |
||||||
|
class Inet4Iterator( |
||||||
|
private val originalIp: InetAddress, |
||||||
|
private val skipOriginal: Boolean = true, |
||||||
|
start: UInt = 2u, |
||||||
|
private val end: UInt = 255u |
||||||
|
) : Iterator<Pair<Int, InetAddress>>, Iterable<Pair<Int, InetAddress>> { |
||||||
|
|
||||||
|
private val a: UByte |
||||||
|
private val b: UByte |
||||||
|
private val c: UByte |
||||||
|
private val skip: UByte |
||||||
|
private var d = start |
||||||
|
|
||||||
|
init { |
||||||
|
val addressBytes = originalIp.address |
||||||
|
a = addressBytes[0].toUByte() |
||||||
|
b = addressBytes[1].toUByte() |
||||||
|
c = addressBytes[2].toUByte() |
||||||
|
skip = addressBytes[3].toUByte() |
||||||
|
} |
||||||
|
|
||||||
|
override fun hasNext(): Boolean { |
||||||
|
return if (skipOriginal && skip == 0xFFu.toUByte()) d < end else d < (end + 1u) |
||||||
|
} |
||||||
|
|
||||||
|
override fun next(): Pair<Int, InetAddress> { |
||||||
|
val result = Pair(d.toInt(), Inet4Util.fromIpClasses(a, b, c, d.toUByte())) |
||||||
|
d += if (d == skip - 1u) 2u else 1u |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
override fun iterator() = this |
||||||
|
} |
||||||
|
|
||||||
|
object Inet4Util { |
||||||
|
fun fromIpClasses(a: UByte, b: UByte, c: UByte, d: UByte): InetAddress { |
||||||
|
return Inet4Address.getByName("$a.$b.$c.$d") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun InetAddress.getIpClassSimilarity(other: InetAddress): Int { |
||||||
|
if (this::class != other::class) return 0 |
||||||
|
val ipClasses = this.address |
||||||
|
val otherIpClasses = other.address |
||||||
|
|
||||||
|
for (i in 0 until ipClasses.size.coerceAtMost(otherIpClasses.size)) { |
||||||
|
if (ipClasses[i] != otherIpClasses[i]) return i |
||||||
|
} |
||||||
|
return ipClasses.size |
||||||
|
} |
||||||
|
|
||||||
|
fun List<InetAddress>.chooseSimilarIp(other: InetAddress) = this.maxByOrNull { it.getIpClassSimilarity(other) } |
||||||
|
|
||||||
|
fun <T> Enumeration<T>.forEach(block: (T) -> Unit) { |
||||||
|
for (i in this) block(i) |
||||||
|
} |
||||||
|
|
||||||
|
fun <T> Enumeration<T>.filter(block: (T) -> Boolean): List<T> { |
||||||
|
val result = mutableListOf<T>() |
||||||
|
for (i in this) if (block(i)) result += i |
||||||
|
return result |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun HttpServletRequest.getIpAddressString(): String = this.getHeader("X-FORWARDED-FOR") ?: this.remoteAddr |
||||||
|
|
||||||
|
fun HttpServletRequest.getIpAddress(): InetAddress = InetAddress.getByName(this.getIpAddressString()) |
||||||
@ -0,0 +1,62 @@ |
|||||||
|
package tv.anypoint.proxy.web |
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest |
||||||
|
import mu.KLogging |
||||||
|
import org.springframework.web.bind.annotation.* |
||||||
|
import tv.anypoint.domain.agent.AuthRequest |
||||||
|
import tv.anypoint.domain.agent.AuthResponse |
||||||
|
import tv.anypoint.domain.agent.StateChangeLog |
||||||
|
import tv.anypoint.domain.agent.ad.AdsResponse |
||||||
|
import tv.anypoint.domain.agent.ad.AdsSyncRequest |
||||||
|
|
||||||
|
@RestController |
||||||
|
@RequestMapping("/v3/device") |
||||||
|
class DeviceController { |
||||||
|
|
||||||
|
@PostMapping("/auth") |
||||||
|
fun authorize(request: HttpServletRequest, @RequestBody body: AuthRequest): AuthResponse { |
||||||
|
logger.info("POST /v3/device/auth -d $body") |
||||||
|
|
||||||
|
// request.getIpAddress() |
||||||
|
val authResponse: AuthResponse = TODO("authRequestHandlerService.handleRequest(HttpRequestUtil.getIpAddress(request), body)") |
||||||
|
authResponse.adConfig.maxDownloadBandwidth = 1000 * 1024 * 1024 |
||||||
|
authResponse.adConfig.maxLazyDownloadBandwidth = 1000 * 1024 * 1024 |
||||||
|
return authResponse |
||||||
|
} |
||||||
|
|
||||||
|
@GetMapping("/ads") |
||||||
|
fun advertisements( |
||||||
|
@RequestParam("deviceId") deviceId: Long, |
||||||
|
@RequestParam("freeStorage", required = false, defaultValue = "10000000000") freeStorage: Long?, |
||||||
|
@RequestParam("usedStorage", required = false, defaultValue = "0") usedStorage: Long? |
||||||
|
): AdsResponse { |
||||||
|
logger.info("GET /v3/device/ads?deviceId=$deviceId&freeStorage=$freeStorage&usedStorage=$usedStorage") |
||||||
|
// return adsRequestHandlerService.handleRequest(deviceId, freeStorage, usedStorage) |
||||||
|
return AdsResponse() |
||||||
|
} |
||||||
|
|
||||||
|
@PostMapping("/sync/ads") |
||||||
|
fun adSync(request: HttpServletRequest, @RequestBody body: AdsSyncRequest) { |
||||||
|
logger.info("POST /v3/device/sync/ads -d $body") |
||||||
|
// deviceAdSyncHandlerService.handleRequest(HttpRequestUtil.getIpAddress(request)) |
||||||
|
} |
||||||
|
|
||||||
|
@PostMapping("/state-logs") |
||||||
|
fun stateLogs(@RequestBody body: List<StateChangeLog>) { |
||||||
|
logger.info("POST /v3/device/state-logs -d $body") |
||||||
|
} |
||||||
|
|
||||||
|
@PostMapping("/event") |
||||||
|
@ResponseBody |
||||||
|
fun deviceEvent(request: HttpServletRequest, @RequestBody body: String) { |
||||||
|
logger.info("POST /v3/device/event -d $body") |
||||||
|
} |
||||||
|
|
||||||
|
@PostMapping("/impression-logs") |
||||||
|
@ResponseBody |
||||||
|
fun impressionLogs(request: HttpServletRequest, @RequestBody body: String) { |
||||||
|
logger.info("POST /v3/device/ssion-logs -d $body") |
||||||
|
} |
||||||
|
|
||||||
|
companion object : KLogging() |
||||||
|
} |
||||||
@ -1,9 +1,24 @@ |
|||||||
spring: |
spring: |
||||||
application: |
application: |
||||||
name: android-qa |
name: android-qa |
||||||
|
profiles: |
||||||
|
active: skb |
||||||
logging: |
logging: |
||||||
level: |
level: |
||||||
root: INFO |
root: INFO |
||||||
tv.anypoint: DEBUG |
tv.anypoint: DEBUG |
||||||
anypoint.android-qa: |
anypoint.android-qa: |
||||||
file-path: /home/bean/dev/qa |
stb: |
||||||
|
ip: 192.168.0.1 |
||||||
|
port: 5555 |
||||||
|
file-root: /home/bean/dev/qa |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
spring: |
||||||
|
config.activate.on-profile: skb |
||||||
|
anypoint.android-qa: |
||||||
|
push-server-port: 31102 |
||||||
|
endpoints: |
||||||
|
auth: https://skb-api.anypoint.tv |
||||||
|
assign: http://skb-assign-app-prod.ap-northeast-1.elasticbeanstalk.com |
||||||
|
|||||||
@ -0,0 +1,4 @@ |
|||||||
|
package tv.anypoint.androidqa |
||||||
|
|
||||||
|
class AdsResponseTest { |
||||||
|
} |
||||||
@ -1,13 +0,0 @@ |
|||||||
package tv.anypoint.androidqa |
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test |
|
||||||
import org.springframework.boot.test.context.SpringBootTest |
|
||||||
|
|
||||||
@SpringBootTest |
|
||||||
class AndroidQaApplicationTests { |
|
||||||
|
|
||||||
@Test |
|
||||||
fun contextLoads() { |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -0,0 +1,4 @@ |
|||||||
|
package tv.anypoint.androidqa |
||||||
|
|
||||||
|
class AssetConvertResponseTest { |
||||||
|
} |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
package tv.anypoint.androidqa |
||||||
|
|
||||||
|
import io.kotest.core.spec.style.BehaviorSpec |
||||||
|
import io.kotest.matchers.shouldBe |
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi |
||||||
|
import kotlinx.serialization.json.Json |
||||||
|
import kotlinx.serialization.json.decodeFromStream |
||||||
|
import org.springframework.core.io.ClassPathResource |
||||||
|
import tv.anypoint.domain.agent.AuthResponse |
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class) |
||||||
|
class AuthResponseTest : BehaviorSpec() { |
||||||
|
|
||||||
|
init { |
||||||
|
Given("auth-response.json file") { |
||||||
|
val file = ClassPathResource("auth-response.json").file |
||||||
|
When("deserialize") { |
||||||
|
val actual = Json.decodeFromStream<AuthResponse>(file.inputStream()) |
||||||
|
Then("read values") { |
||||||
|
actual.deviceId shouldBe 36986237L |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
package tv.anypoint.androidqa |
||||||
|
|
||||||
|
class VastResponseTest { |
||||||
|
} |
||||||
@ -0,0 +1,98 @@ |
|||||||
|
{ |
||||||
|
"status": "OK", |
||||||
|
"ads": [ |
||||||
|
{ |
||||||
|
"id": 66192, |
||||||
|
"adRequest": null, |
||||||
|
"asset": { |
||||||
|
"assetId": 724560, |
||||||
|
"crc": "1081046660", |
||||||
|
"mediaUrl": "https://ecdnaddtv.hanafostv.com:9443/addrads/prod/72456/ENCODING/18/1672300537007.ts", |
||||||
|
"duration": 14948, |
||||||
|
"bytes": 7238188, |
||||||
|
"playType": "DNP", |
||||||
|
"extendedQueryString": "vid_bitrate=3817000&vid_color_bits=8&vid_codec=27&vid_profile=100&vid_level=40&vid_height=1080&vid_id=481&vid_scan=4&vid_width=1920&aud1_bitrate=3817000&aud1_ch=2&aud1_codec=15&aud1_profile=LC&aud1_id=482&aud1_sr=48000&aud2_bitrate=3817000&aud2_ch=2&aud2_codec=129&aud2_id=483&aud2_sr=48000&" |
||||||
|
}, |
||||||
|
"campaignId": 26627, |
||||||
|
"campaignType": "CHARGE", |
||||||
|
"viewSequence": 5400, |
||||||
|
"paused": false, |
||||||
|
"price": 0, |
||||||
|
"priceModel": "CPV", |
||||||
|
"priceCurrency": "KRW", |
||||||
|
"startAt": 1675177200000, |
||||||
|
"endAt": 2145884399999, |
||||||
|
"extraPlay": 0, |
||||||
|
"extraPlayContinuous": false |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": 48341, |
||||||
|
"adRequest": null, |
||||||
|
"asset": { |
||||||
|
"assetId": 684610, |
||||||
|
"crc": "1859527903", |
||||||
|
"mediaUrl": "https://ecdnaddtv.hanafostv.com:9443/addrads/prod/68457/ENCODING/18/1668564528712.ts", |
||||||
|
"duration": 2936, |
||||||
|
"bytes": 498576, |
||||||
|
"playType": "DNP", |
||||||
|
"extendedQueryString": "vid_bitrate=1334000&vid_color_bits=8&vid_codec=27&vid_profile=100&vid_level=40&vid_height=1080&vid_id=481&vid_scan=4&vid_width=1920&aud1_bitrate=1334000&aud1_ch=2&aud1_codec=15&aud1_profile=LC&aud1_id=482&aud1_sr=48000&aud2_bitrate=1334000&aud2_ch=2&aud2_codec=129&aud2_id=483&aud2_sr=48000&" |
||||||
|
}, |
||||||
|
"campaignId": 2645, |
||||||
|
"campaignType": "ENDING", |
||||||
|
"viewSequence": 0, |
||||||
|
"paused": false, |
||||||
|
"price": 0, |
||||||
|
"priceModel": "CPV", |
||||||
|
"priceCurrency": "KRW", |
||||||
|
"startAt": 1606834800000, |
||||||
|
"endAt": 2145884399999, |
||||||
|
"extraPlay": 0, |
||||||
|
"extraPlayContinuous": false |
||||||
|
} |
||||||
|
], |
||||||
|
"targetAds": [ |
||||||
|
{ |
||||||
|
"hours": "|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|", |
||||||
|
"days": "|1|2|3|4|5|6|7|", |
||||||
|
"placements": "|100-1|109-1|112-2|115-1|118-1|121-1|124-1|133-1|136-1|142-1|154-1|157-1|160-2|163-1|169-1|172-1|178-1|181-1|184-1|187-1|190-1|193-1|196-1|199-1|205-1|208-1|211-1|223-1|232-1|235-1|238-1|241-1|244-1|247-1|250-1|253-1|307-1|316-2|325-2|328-1|337-1|340-2|343-1|346-1|349-1|352-1|355-1|361-1|364-1|367-1|37-1|40-1|424-1|427-1|43-1|430-1|433-1|436-1|46-1|469-1|49-1|499-2|511-1|512-1|514-1|515-1|524-1|541-1|728-1|773-1|774-1|784-1|79-1|809-1|82-1|85-1|88-1|883-1|885-1|887-200|888-200|889-1|91-1|94-1|97-1|", |
||||||
|
"adIds": "|66437|" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"hours": "|0|1|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|", |
||||||
|
"days": "|1|2|3|4|5|6|7|", |
||||||
|
"placements": "|262-1|379-1|382-1|385-1|388-1|397-1|406-1|409-1|415-2|418-1|421-1|", |
||||||
|
"adIds": "|66192|" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"hours": "|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|", |
||||||
|
"days": "|1|2|3|4|5|6|7|", |
||||||
|
"placements": "|322-1000|322-1100|322-1200|322-1300|322-2000|322-2100|322-2200|322-2300|322-3000|322-3100|322-3200|322-3300|", |
||||||
|
"adIds": "|-54695|-54694|" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"hours": "|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|", |
||||||
|
"days": "|1|2|3|4|5|6|7|", |
||||||
|
"placements": "|13-100|13-110|13-120|13-130|", |
||||||
|
"adIds": "|-54561|-54560|-54559|-54558|-54557|-54556|" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"hours": "|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|", |
||||||
|
"days": "|1|2|3|4|5|6|7|", |
||||||
|
"placements": "|190-2|", |
||||||
|
"adIds": "|-53060|" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"hours": "|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|", |
||||||
|
"days": "|1|2|3|4|5|6|7|", |
||||||
|
"placements": "|100-1|109-1|109-2|112-2|112-5|115-1|118-1|121-1|121-2|124-1|133-1|136-1|139-1|142-1|154-1|157-1|160-2|163-1|169-1|172-1|175-1|178-1|181-1|184-1|187-1|190-1|190-2|193-1|196-1|199-1|205-1|208-1|211-1|211-2|214-1|217-1|217-2|220-1|223-1|226-1|229-1|232-1|235-1|235-2|238-1|241-1|241-2|244-1|244-2|247-1|250-1|250-2|253-1|253-2|259-1|262-1|274-1|277-1|289-1|298-1|307-1|310-1|316-2|316-5|322-1|322-1000|322-1100|322-1200|322-1300|322-2000|322-2100|322-2200|322-2300|322-3000|322-3100|322-3200|322-3300|325-2|325-5|328-1|331-1|337-1|340-2|340-5|343-1|343-2|346-1|349-1|349-2|352-1|355-1|358-1|361-1|364-1|367-1|37-1|379-1|379-2|382-1|385-1|388-1|397-1|40-1|406-1|409-1|415-2|415-5|418-1|421-1|424-1|427-1|43-1|430-1|433-1|436-1|46-1|469-1|469-5|487-1|49-1|499-2|511-1|512-1|514-1|514-2|515-1|524-1|524-2|541-1|545-2|687-2|691-1|691-1000|691-1100|691-1200|691-1300|691-2000|691-2100|691-2200|691-2300|691-3000|691-3100|691-3200|691-3300|723-2|728-1|728-2|734-2|735-2|736-2|773-1|773-2|774-1|784-1|79-1|79-2|796-2|797-2|809-1|809-2|814-2|82-1|85-1|88-1|883-1|883-2|885-1|885-1200|885-2200|885-3200|887-1000|887-1100|887-1200|887-1300|887-200|887-2000|887-2100|887-2200|887-2300|887-3000|887-3100|887-3200|887-3300|888-1100|888-1200|888-1300|888-2|888-200|889-1|889-1000|889-2000|889-3000|891-1200|891-2200|891-3200|91-1|94-1|97-1|", |
||||||
|
"adIds": "|45174|45175|48490|52835|66003|-54714|" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"hours": "|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|", |
||||||
|
"days": "|1|2|3|4|5|6|7|", |
||||||
|
"placements": "|10-100|10-110|10-120|10-130|100-1|109-1|109-2|112-2|112-5|115-1|118-1|121-1|121-2|124-1|13-10|13-100|13-110|13-120|13-130|133-1|136-1|139-1|142-1|154-1|154-5|157-1|160-2|160-5|163-1|169-1|172-1|175-1|178-1|181-1|184-1|187-1|190-1|190-2|193-1|196-1|199-1|205-1|208-1|211-1|211-2|214-1|217-1|217-2|220-1|223-1|226-1|229-1|232-1|235-1|235-2|238-1|241-1|241-2|244-1|244-2|247-1|250-1|250-2|253-1|253-2|259-1|262-1|274-1|277-1|289-1|298-1|307-1|310-1|316-2|316-5|322-1|322-1000|322-1100|322-1200|322-1300|322-2000|322-2100|322-2200|322-2300|322-3000|322-3100|322-3200|322-3300|325-2|325-5|328-1|331-1|337-1|340-2|340-5|343-1|343-2|346-1|349-1|349-2|352-1|355-1|358-1|361-1|364-1|367-1|37-1|379-1|379-2|382-1|385-1|388-1|397-1|40-1|406-1|409-1|415-2|415-5|418-1|421-1|424-1|427-1|43-1|430-1|433-1|436-1|46-1|469-1|469-5|487-1|49-1|499-2|499-5|511-1|512-1|514-1|514-2|515-1|524-1|524-2|541-1|545-2|687-2|691-1|691-1000|691-1100|691-1200|691-1300|691-2000|691-2100|691-2200|691-2300|691-3000|691-3100|691-3200|691-3300|723-2|728-1|728-2|734-2|735-2|736-2|773-1|773-2|774-1|784-1|79-1|79-2|796-2|797-2|809-1|809-2|814-2|82-1|85-1|88-1|883-1|883-2|885-1|885-1200|885-2200|885-3200|887-1000|887-1100|887-1200|887-1300|887-200|887-2000|887-2100|887-2200|887-2300|887-3000|887-3100|887-3200|887-3300|888-1100|888-1200|888-1300|888-2|888-200|889-1|889-1000|889-2000|889-3000|891-1200|891-2200|891-3200|91-1|94-1|97-1|", |
||||||
|
"adIds": "|48341|66003|67280|66192|66361|66133|66577|66260|-54714|67368|67367|-55857|67149|67147|67343|67370|" |
||||||
|
} |
||||||
|
], |
||||||
|
"deleteCache": false |
||||||
|
} |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
{ |
||||||
|
"created": false, |
||||||
|
"asset": { |
||||||
|
"assetId": 30116, |
||||||
|
"crc": "1624645205", |
||||||
|
"duration": 15000, |
||||||
|
"mediaUrl": "http://123.140.50.10/prod/30116/ENCODING/18/1646959017272.mp4", |
||||||
|
"playType": "DNP", |
||||||
|
"bytes": 6741195, |
||||||
|
"extendedQueryString": "audioPID=1002&audioType=AAC&videoPID=1001&videoType=H.264" |
||||||
|
} |
||||||
|
} |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,64 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
||||||
|
<VAST version="3.0" xmlns="http://www.iab.net/videosuite/vast"> |
||||||
|
<Ad id="labZCxzVezmmzCFJ..leyIgUkNyTxpxAr2"> |
||||||
|
<InLine> |
||||||
|
<AdSystem version="2.0"></AdSystem> |
||||||
|
<AdTitle> |
||||||
|
<![CDATA[labZCxzVezmmzCFJ..leyIgUkNyTxpxAr2]]> |
||||||
|
</AdTitle> |
||||||
|
<Description> |
||||||
|
<![CDATA[]]> |
||||||
|
</Description> |
||||||
|
<Pricing model="CPM" currency="USD">5.101</Pricing> |
||||||
|
<Survey> |
||||||
|
<![CDATA[]]> |
||||||
|
</Survey> |
||||||
|
<Impression> |
||||||
|
<![CDATA[http://192.168.10.13/grafter/imp?exchange=FLOWER&info=ChD_DOlhfBdKt6LFKb5wetkzEKG2ypoGGigICiIkMTJhZTQwNzUtZTA4MC01NjI1LWE4OTItZGFiZWFlZmUyMTNlIAIqADIA&campaign_name=rK5xhArs7Pvy2baC&imp_id=fg.req.id-34c0f17f-2440-42c0-bee0-3d2149aefc9b&price=5.101]]> |
||||||
|
</Impression> |
||||||
|
<Creatives> |
||||||
|
<Creative> |
||||||
|
<Linear> |
||||||
|
<Duration>00:00:15</Duration> |
||||||
|
<TrackingEvents> |
||||||
|
<Tracking event="firstQuartile"> |
||||||
|
<![CDATA[http://192.168.10.13/grafter/imp_extra?exchange=FLOWER&info=ChD_DOlhfBdKt6LFKb5wetkzEKG2ypoGGigICiIkMTJhZTQwNzUtZTA4MC01NjI1LWE4OTItZGFiZWFlZmUyMTNlIAIqADIA&event_type=play_1q]]> |
||||||
|
</Tracking> |
||||||
|
<Tracking event="midpoint"> |
||||||
|
<![CDATA[http://192.168.10.13/grafter/imp_extra?exchange=FLOWER&info=ChD_DOlhfBdKt6LFKb5wetkzEKG2ypoGGigICiIkMTJhZTQwNzUtZTA4MC01NjI1LWE4OTItZGFiZWFlZmUyMTNlIAIqADIA&event_type=play_2q]]> |
||||||
|
</Tracking> |
||||||
|
<Tracking event="thirdQuartile"> |
||||||
|
<![CDATA[http://192.168.10.13/grafter/imp_extra?exchange=FLOWER&info=ChD_DOlhfBdKt6LFKb5wetkzEKG2ypoGGigICiIkMTJhZTQwNzUtZTA4MC01NjI1LWE4OTItZGFiZWFlZmUyMTNlIAIqADIA&event_type=play_3q]]> |
||||||
|
</Tracking> |
||||||
|
<Tracking event="complete"> |
||||||
|
<![CDATA[http://192.168.10.13/grafter/imp_extra?exchange=FLOWER&info=ChD_DOlhfBdKt6LFKb5wetkzEKG2ypoGGigICiIkMTJhZTQwNzUtZTA4MC01NjI1LWE4OTItZGFiZWFlZmUyMTNlIAIqADIA&event_type=play_4q]]> |
||||||
|
</Tracking> |
||||||
|
<Tracking event="start"> |
||||||
|
<![CDATA[http://192.168.10.13/grafter/start?id=fg.req.id-34c0f17f-2440-42c0-bee0-3d2149aefc9b&p=4&au=31&dsp=5&c=labZCxzVezmmzCFJ..leyIgUkNyTxpxAr2]]> |
||||||
|
</Tracking> |
||||||
|
</TrackingEvents> |
||||||
|
<MediaFiles> |
||||||
|
<MediaFile id="labZCxzVezmmzCFJ..leyIgUkNyTxpxAr2" delivery="progressive" type="video/mp4" width="1920" height="1080"> |
||||||
|
<![CDATA[http://123.140.50.10/prod/37084/ENCODING/18/138952160385610248.mp4]]> |
||||||
|
</MediaFile> |
||||||
|
</MediaFiles> |
||||||
|
</Linear> |
||||||
|
</Creative> |
||||||
|
<Creative> |
||||||
|
<Linear> |
||||||
|
<TrackingEvents/> |
||||||
|
<VideoClicks/> |
||||||
|
<MediaFiles/> |
||||||
|
</Linear> |
||||||
|
</Creative> |
||||||
|
</Creatives> |
||||||
|
<Extensions> |
||||||
|
<Extension type="FlowerAsset"> |
||||||
|
<Asset xmlns=""> |
||||||
|
{"assetId":-50430,"crc":"3113586070","duration":14981,"mediaUrl":"http://123.140.50.75/prod/-50430/ENCODING/18/138961747812860183.mp4","playType":"DNP","bytes":5702218} |
||||||
|
</Asset> |
||||||
|
</Extension> |
||||||
|
</Extensions> |
||||||
|
</InLine> |
||||||
|
</Ad> |
||||||
|
</VAST> |
||||||
Loading…
Reference in new issue