diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6537ca4 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..898b420 --- /dev/null +++ b/README.md @@ -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) 클릭 + + ![img.png](img.png) + 3. video capture device(V4L2) (리눅스 기준) 클릭 + 4. 디바이스를 바꿔가면서 STB 를 찾아 선택 후 OK 클릭 + + ![img_1.png](img_1.png) + +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://: + ``` +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 `` 의 형식으로 입력. +- 오디오: -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 +``` + diff --git a/build.gradle.kts b/build.gradle.kts index 5eede32..346e554 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,10 +21,20 @@ repositories { dependencies { implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.cloud:spring-cloud-starter-openfeign") implementation("org.jetbrains.kotlin:kotlin-reflect") + testImplementation("org.springframework.boot:spring-boot-starter-test") implementation(libs.bundles.deps) + testImplementation(libs.bundles.test.deps) +} + +dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:2023.0.1") + } } tasks.withType { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 31d7f4f..2fe63f0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,18 +5,27 @@ kotlin = "1.9.23" jadb = { module = "com.github.vidstige:jadb", version = "v1.2.1" } # adb netty = { module = "io.netty:netty-all", version = "4.1.96.Final" } kotlinx-serializable = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.6.0" } -kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.5.0" } +kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.8.0" } kotlin-logging = { module = "io.github.microutils:kotlin-logging-jvm", version = "2.0.11" } thymeleaf = { module = "org.thymeleaf:thymeleaf", version = "3.1.2.RELEASE" } thymeleaf-spring5 = { module = "org.thymeleaf:thymeleaf-spring5", version = "3.1.2.RELEASE" } +kotest-runner = { module = "io.kotest:kotest-runner-junit5-jvm", version = "5.8.1" } +kotest-property = { module = "io.kotest:kotest-property", version = "5.8.1" } +kotest-extensions-string = { module = "io.kotest.extensions:kotest-extensions-spring", version = "1.1.3" } + [bundles] deps = [ "jadb", "netty", "kotlinx-serializable", - "kotlinx-datetime", "kotlin-logging", "thymeleaf", - "thymeleaf-spring5" -] \ No newline at end of file + "thymeleaf-spring5", + "kotlinx-coroutines" +] +test-deps = [ + "kotest-runner", + "kotest-property", + "kotest-extensions-string" +] diff --git a/img.png b/img.png new file mode 100644 index 0000000..8199986 Binary files /dev/null and b/img.png differ diff --git a/img_1.png b/img_1.png new file mode 100644 index 0000000..37e628e Binary files /dev/null and b/img_1.png differ diff --git a/src/main/kotlin/tv/anypoint/ApplicationProperties.kt b/src/main/kotlin/tv/anypoint/ApplicationProperties.kt index a2fc59a..a6111ff 100644 --- a/src/main/kotlin/tv/anypoint/ApplicationProperties.kt +++ b/src/main/kotlin/tv/anypoint/ApplicationProperties.kt @@ -6,5 +6,24 @@ import org.springframework.stereotype.Component @Component @ConfigurationProperties(prefix = "anypoint.android-qa") class ApplicationProperties { - final lateinit var filePath: String + val stb: Stb = Stb() + + /** + * STB 의 logcat 을 덤프할 위치, 녹화된 영상을 저장할 위치 + */ + final lateinit var fileRoot: String + + final var pushServerPort: Int = 31102 + + val endpoints: Endpoints = Endpoints() + + class Stb { + lateinit var ip: String + var port: Int = 5555 + } + + class Endpoints { + lateinit var auth: String + lateinit var assign: String + } } \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/domain/adb/ChangeCommand.kt b/src/main/kotlin/tv/anypoint/domain/adb/ChangeCommand.kt new file mode 100644 index 0000000..4f1c583 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/adb/ChangeCommand.kt @@ -0,0 +1,6 @@ +package tv.anypoint.domain.adb + +enum class ChangeCommand { + CHANGE_API_ENDPOINT, + CHANGE_TEST_FLAG +} diff --git a/src/main/kotlin/tv/anypoint/dsl/model/adb/ExtraKey.kt b/src/main/kotlin/tv/anypoint/domain/adb/ExtraKey.kt similarity index 83% rename from src/main/kotlin/tv/anypoint/dsl/model/adb/ExtraKey.kt rename to src/main/kotlin/tv/anypoint/domain/adb/ExtraKey.kt index e504418..2df7283 100644 --- a/src/main/kotlin/tv/anypoint/dsl/model/adb/ExtraKey.kt +++ b/src/main/kotlin/tv/anypoint/domain/adb/ExtraKey.kt @@ -1,4 +1,4 @@ -package tv.anypoint.dsl.model.adb +package tv.anypoint.domain.adb enum class ExtraKey(val string: String) { EXTRA_JSON("EXTRA_JSON"), diff --git a/src/main/kotlin/tv/anypoint/dsl/model/adb/IntentAction.kt b/src/main/kotlin/tv/anypoint/domain/adb/IntentAction.kt similarity index 74% rename from src/main/kotlin/tv/anypoint/dsl/model/adb/IntentAction.kt rename to src/main/kotlin/tv/anypoint/domain/adb/IntentAction.kt index 6d59c57..7a12bb7 100644 --- a/src/main/kotlin/tv/anypoint/dsl/model/adb/IntentAction.kt +++ b/src/main/kotlin/tv/anypoint/domain/adb/IntentAction.kt @@ -1,6 +1,5 @@ -package tv.anypoint.dsl.model.adb +package tv.anypoint.domain.adb enum class IntentAction(val string: String) { CHANGE_TEST_PROPERTY("tv.anypoint.agent.app.CHANGE_TEST_PROPERTY") - ; } \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/domain/agent/AuthRequest.kt b/src/main/kotlin/tv/anypoint/domain/agent/AuthRequest.kt new file mode 100644 index 0000000..eb05ef1 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/AuthRequest.kt @@ -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, +) diff --git a/src/main/kotlin/tv/anypoint/domain/agent/AuthResponse.kt b/src/main/kotlin/tv/anypoint/domain/agent/AuthResponse.kt new file mode 100644 index 0000000..3b60659 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/AuthResponse.kt @@ -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 = 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 = emptyList(), + var stateLog: String = "", + var impressionLog: String = "", + var ntpServers: List = 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 +) \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/domain/agent/Cue.kt b/src/main/kotlin/tv/anypoint/domain/agent/Cue.kt new file mode 100644 index 0000000..a002159 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/Cue.kt @@ -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 + } + } +} diff --git a/src/main/kotlin/tv/anypoint/domain/agent/CueOwner.kt b/src/main/kotlin/tv/anypoint/domain/agent/CueOwner.kt new file mode 100644 index 0000000..224eb57 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/CueOwner.kt @@ -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 } + } + } +} diff --git a/src/main/kotlin/tv/anypoint/domain/agent/PlayType.kt b/src/main/kotlin/tv/anypoint/domain/agent/PlayType.kt new file mode 100644 index 0000000..e923051 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/PlayType.kt @@ -0,0 +1,7 @@ +package tv.anypoint.domain.agent + +enum class PlayType { + DNP, + LAZY_DNP, + STREAMING +} \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/domain/agent/PlayerStateCheckParam.kt b/src/main/kotlin/tv/anypoint/domain/agent/PlayerStateCheckParam.kt new file mode 100644 index 0000000..f334fa4 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/PlayerStateCheckParam.kt @@ -0,0 +1,6 @@ +package tv.anypoint.domain.agent + +data class PlayerStateCheckParam( + val playerStateCheckDuration: Int = 1500, + val maxPlayerInvalidateInterval: Int = 300 +) \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/domain/agent/ProgramProviderChannel.kt b/src/main/kotlin/tv/anypoint/domain/agent/ProgramProviderChannel.kt new file mode 100644 index 0000000..c737536 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/ProgramProviderChannel.kt @@ -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? = 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 + } +} diff --git a/src/main/kotlin/tv/anypoint/domain/agent/StateChangeLog.kt b/src/main/kotlin/tv/anypoint/domain/agent/StateChangeLog.kt new file mode 100644 index 0000000..f6167c8 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/StateChangeLog.kt @@ -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 +} diff --git a/src/main/kotlin/tv/anypoint/domain/agent/ad/Ad.kt b/src/main/kotlin/tv/anypoint/domain/agent/ad/Ad.kt new file mode 100644 index 0000000..194a00b --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/ad/Ad.kt @@ -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? = null +) + +enum class CampaignType { + CHARGE, + FREE, + HOUSE, + BUFFER, + ENDING +} \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/domain/agent/ad/AdRequest.kt b/src/main/kotlin/tv/anypoint/domain/agent/ad/AdRequest.kt new file mode 100644 index 0000000..8eed862 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/ad/AdRequest.kt @@ -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? = null, + var fallbackUrl: String? = null, + val fallbackProtocol: ExtAdProtocol = ExtAdProtocol.VAST, +) diff --git a/src/main/kotlin/tv/anypoint/domain/agent/ad/AdsResponse.kt b/src/main/kotlin/tv/anypoint/domain/agent/ad/AdsResponse.kt new file mode 100644 index 0000000..eb05383 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/ad/AdsResponse.kt @@ -0,0 +1,14 @@ +package tv.anypoint.domain.agent.ad + +data class AdsResponse( + val status: AdListResponseStatus = AdListResponseStatus.OK, + val ads: List = listOf(), + val targetAds: List = listOf(), + val deleteCache: Boolean = false, +) + +enum class AdListResponseStatus { + OK, + ERROR, + IGNORE_ERROR +} diff --git a/src/main/kotlin/tv/anypoint/domain/agent/ad/AdsSyncRequest.kt b/src/main/kotlin/tv/anypoint/domain/agent/ad/AdsSyncRequest.kt new file mode 100644 index 0000000..daebc73 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/ad/AdsSyncRequest.kt @@ -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 +) diff --git a/src/main/kotlin/tv/anypoint/domain/agent/ad/Asset.kt b/src/main/kotlin/tv/anypoint/domain/agent/ad/Asset.kt new file mode 100644 index 0000000..2c313b3 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/ad/Asset.kt @@ -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 = "" +) diff --git a/src/main/kotlin/tv/anypoint/domain/agent/ad/AssetConvertResponse.kt b/src/main/kotlin/tv/anypoint/domain/agent/ad/AssetConvertResponse.kt new file mode 100644 index 0000000..b1829b2 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/ad/AssetConvertResponse.kt @@ -0,0 +1,3 @@ +package tv.anypoint.domain.agent.ad + +data class AssetConvertResponse(val id: Long) \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/domain/agent/ad/Click.kt b/src/main/kotlin/tv/anypoint/domain/agent/ad/Click.kt new file mode 100644 index 0000000..aa55ef3 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/ad/Click.kt @@ -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 +) diff --git a/src/main/kotlin/tv/anypoint/domain/agent/ad/ProgramPlacement.kt b/src/main/kotlin/tv/anypoint/domain/agent/ad/ProgramPlacement.kt new file mode 100644 index 0000000..f2e2445 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/ad/ProgramPlacement.kt @@ -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 +} \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/domain/agent/ad/TargetAd.kt b/src/main/kotlin/tv/anypoint/domain/agent/ad/TargetAd.kt new file mode 100644 index 0000000..11b0555 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/ad/TargetAd.kt @@ -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 = (0..23).toSet(), + days: Set = (1..7).toSet(), + placements: Set = emptySet(), + adIds: Set = emptySet() + ) = TargetAd( + hours = hours.joinWithVerticalBar(), + days = days.joinWithVerticalBar(), + placements = placements + .map { "${it.placementUniqueKey.contentId}-${it.placementUniqueKey.programId}" } + .joinWithVerticalBar(), + adIds = adIds.joinWithVerticalBar() + ) + } +} + +private fun Iterable.joinWithVerticalBar() = this.joinToString(prefix = "|", postfix = "|", separator = "|") diff --git a/src/main/kotlin/tv/anypoint/domain/agent/ad/VastResponse.kt b/src/main/kotlin/tv/anypoint/domain/agent/ad/VastResponse.kt new file mode 100644 index 0000000..8d2d897 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/domain/agent/ad/VastResponse.kt @@ -0,0 +1,3 @@ +package tv.anypoint.domain.agent.ad + +data class VastResponse(val id: Long) \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/dsl/Dsl.kt b/src/main/kotlin/tv/anypoint/dsl/Dsl.kt index b2e40de..64ccb0d 100644 --- a/src/main/kotlin/tv/anypoint/dsl/Dsl.kt +++ b/src/main/kotlin/tv/anypoint/dsl/Dsl.kt @@ -1,22 +1,19 @@ package tv.anypoint.dsl +import tv.anypoint.domain.adb.ExtraKey +import tv.anypoint.domain.adb.IntentAction import tv.anypoint.dsl.handler.HttpHandler -import tv.anypoint.dsl.model.adb.ExtraKey -import tv.anypoint.dsl.model.adb.IntentAction import tv.anypoint.dsl.model.Tc import tv.anypoint.dsl.service.TestCase -import java.time.Duration import java.time.LocalDateTime -import java.time.Period import java.time.temporal.ChronoUnit -import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException inline fun tc( block: Tc.() -> Unit ) = Tc().also { block(it) } -inline fun http( +inline fun http( block: HttpHandler.() -> Unit ) = HttpHandler().also { block(it) } @@ -47,4 +44,4 @@ fun TestCase.expected( } // TODO: tc 로그에서 expectedLog 찾기 tc.logInfo.cursor ~ 마지막 라인까지의 로그 중에 찾으면 됨 } -} \ No newline at end of file +} diff --git a/src/main/kotlin/tv/anypoint/dsl/exception/HttpValidationException.kt b/src/main/kotlin/tv/anypoint/dsl/exception/HttpValidationException.kt index f3f99fc..76dadca 100644 --- a/src/main/kotlin/tv/anypoint/dsl/exception/HttpValidationException.kt +++ b/src/main/kotlin/tv/anypoint/dsl/exception/HttpValidationException.kt @@ -3,10 +3,10 @@ package tv.anypoint.dsl.exception import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.encodeToJsonElement -import tv.anypoint.dsl.model.http.AdsResponse -import tv.anypoint.dsl.model.http.AssetConvertResponse -import tv.anypoint.dsl.model.http.AuthResponse -import tv.anypoint.dsl.model.http.VastResponse +import tv.anypoint.domain.agent.AuthResponse +import tv.anypoint.domain.agent.ad.AdsResponse +import tv.anypoint.domain.agent.ad.AssetConvertResponse +import tv.anypoint.domain.agent.ad.VastResponse class HttpValidationException( type: HttpValidationExceptionType, @@ -26,7 +26,7 @@ enum class HttpValidationExceptionType { inline fun httpValidationError( response: T ): HttpValidationException = HttpValidationException( - type = when(T::class) { + type = when (T::class) { AuthResponse::class -> HttpValidationExceptionType.AUTH AdsResponse::class -> HttpValidationExceptionType.ADS VastResponse::class -> HttpValidationExceptionType.VAST diff --git a/src/main/kotlin/tv/anypoint/dsl/handler/HttpHandler.kt b/src/main/kotlin/tv/anypoint/dsl/handler/HttpHandler.kt index 5d852b9..613b58f 100644 --- a/src/main/kotlin/tv/anypoint/dsl/handler/HttpHandler.kt +++ b/src/main/kotlin/tv/anypoint/dsl/handler/HttpHandler.kt @@ -1,6 +1,6 @@ package tv.anypoint.dsl.handler -class HttpHandler ( +class HttpHandler( var convert: (response: R) -> R = { it }, var validate: (response: R) -> Boolean = { true } ) \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/dsl/model/RecordingInfo.kt b/src/main/kotlin/tv/anypoint/dsl/model/RecordingInfo.kt index e6af6d2..93280ba 100644 --- a/src/main/kotlin/tv/anypoint/dsl/model/RecordingInfo.kt +++ b/src/main/kotlin/tv/anypoint/dsl/model/RecordingInfo.kt @@ -1,10 +1,14 @@ package tv.anypoint.dsl.model +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable import java.time.LocalDateTime +@Serializable data class RecordingInfo( val absoluteFilePath: String ) { + @Contextual private val createdAt: LocalDateTime = LocalDateTime.now() /** @@ -12,6 +16,7 @@ data class RecordingInfo( */ private var fileSize: Long = 0 + @Contextual private var finishedAt: LocalDateTime? = null fun finish(fileSize: Long) { diff --git a/src/main/kotlin/tv/anypoint/dsl/model/Tc.kt b/src/main/kotlin/tv/anypoint/dsl/model/Tc.kt index 75d316d..b2901c3 100644 --- a/src/main/kotlin/tv/anypoint/dsl/model/Tc.kt +++ b/src/main/kotlin/tv/anypoint/dsl/model/Tc.kt @@ -1,15 +1,15 @@ package tv.anypoint.dsl.model +import tv.anypoint.domain.agent.AuthResponse +import tv.anypoint.domain.agent.ad.AdsResponse +import tv.anypoint.domain.agent.ad.AssetConvertResponse +import tv.anypoint.domain.agent.ad.VastResponse import tv.anypoint.dsl.handler.HttpHandler import tv.anypoint.dsl.http -import tv.anypoint.dsl.model.http.AdsResponse -import tv.anypoint.dsl.model.http.AssetConvertResponse -import tv.anypoint.dsl.model.http.AuthResponse -import tv.anypoint.dsl.model.http.VastResponse import java.time.LocalDateTime class Tc { - var number: Int? = null + var number: String? = null var reboot: Boolean = false var auth: HttpHandler = http {} diff --git a/src/main/kotlin/tv/anypoint/dsl/model/http/AdsResponse.kt b/src/main/kotlin/tv/anypoint/dsl/model/http/AdsResponse.kt deleted file mode 100644 index 4761320..0000000 --- a/src/main/kotlin/tv/anypoint/dsl/model/http/AdsResponse.kt +++ /dev/null @@ -1,6 +0,0 @@ -package tv.anypoint.dsl.model.http - -import kotlinx.serialization.Serializable - -@Serializable -data class AdsResponse(val id: Long) \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/dsl/model/http/AssetConvertResponse.kt b/src/main/kotlin/tv/anypoint/dsl/model/http/AssetConvertResponse.kt deleted file mode 100644 index bd385bd..0000000 --- a/src/main/kotlin/tv/anypoint/dsl/model/http/AssetConvertResponse.kt +++ /dev/null @@ -1,6 +0,0 @@ -package tv.anypoint.dsl.model.http - -import kotlinx.serialization.Serializable - -@Serializable -data class AssetConvertResponse(val id: Long) \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/dsl/model/http/AuthResponse.kt b/src/main/kotlin/tv/anypoint/dsl/model/http/AuthResponse.kt deleted file mode 100644 index 6a6f722..0000000 --- a/src/main/kotlin/tv/anypoint/dsl/model/http/AuthResponse.kt +++ /dev/null @@ -1,6 +0,0 @@ -package tv.anypoint.dsl.model.http - -import kotlinx.serialization.Serializable - -@Serializable -data class AuthResponse(val id: Long) \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/dsl/model/http/VastResponse.kt b/src/main/kotlin/tv/anypoint/dsl/model/http/VastResponse.kt deleted file mode 100644 index 1ea8e67..0000000 --- a/src/main/kotlin/tv/anypoint/dsl/model/http/VastResponse.kt +++ /dev/null @@ -1,6 +0,0 @@ -package tv.anypoint.dsl.model.http - -import kotlinx.serialization.Serializable - -@Serializable -data class VastResponse(val id: Long) \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/dsl/serialization/JsonConfiguration.kt b/src/main/kotlin/tv/anypoint/dsl/serialization/JsonConfiguration.kt new file mode 100644 index 0000000..2a6beed --- /dev/null +++ b/src/main/kotlin/tv/anypoint/dsl/serialization/JsonConfiguration.kt @@ -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()) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/dsl/serialization/LocalDateSerializer.kt b/src/main/kotlin/tv/anypoint/dsl/serialization/LocalDateSerializer.kt new file mode 100644 index 0000000..54e8061 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/dsl/serialization/LocalDateSerializer.kt @@ -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 { + 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()) +} \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/dsl/serialization/LocalDateTimeSerializer.kt b/src/main/kotlin/tv/anypoint/dsl/serialization/LocalDateTimeSerializer.kt new file mode 100644 index 0000000..b0ece5d --- /dev/null +++ b/src/main/kotlin/tv/anypoint/dsl/serialization/LocalDateTimeSerializer.kt @@ -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 { + 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()) +} \ No newline at end of file diff --git a/src/main/kotlin/tv/anypoint/dsl/service/TestCase.kt b/src/main/kotlin/tv/anypoint/dsl/service/TestCase.kt index 95f5044..5c989cb 100644 --- a/src/main/kotlin/tv/anypoint/dsl/service/TestCase.kt +++ b/src/main/kotlin/tv/anypoint/dsl/service/TestCase.kt @@ -4,13 +4,13 @@ import mu.KLogging import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component import tv.anypoint.ApplicationProperties +import tv.anypoint.domain.agent.AuthResponse +import tv.anypoint.domain.agent.ad.AdsResponse +import tv.anypoint.domain.agent.ad.AssetConvertResponse +import tv.anypoint.domain.agent.ad.VastResponse import tv.anypoint.dsl.exception.httpValidationError import tv.anypoint.dsl.model.LogInfo import tv.anypoint.dsl.model.RecordingInfo -import tv.anypoint.dsl.model.http.AdsResponse -import tv.anypoint.dsl.model.http.AssetConvertResponse -import tv.anypoint.dsl.model.http.AuthResponse -import tv.anypoint.dsl.model.http.VastResponse import tv.anypoint.dsl.model.Tc import java.time.LocalDateTime @@ -32,18 +32,19 @@ abstract class TestCase { private lateinit var captureBoardRecorder: CaptureBoardRecorder fun executeTest() { + startToDumpLog() tc = init() - logger.info("[TC-${tc.number}] starting...") + logger.info("[${tc.number}] starting...") try { if (tc.reboot) { - logger.info("[TC-${tc.number}] reboot STB") + logger.info("[${tc.number}] reboot STB") // TODO } else { - logger.info("[TC-${tc.number}] get systemInfo STB") + logger.info("[${tc.number}] get systemInfo STB") // TODO } - startToDumpLog() + auth() ads() @@ -55,23 +56,23 @@ abstract class TestCase { stopToDumpLog() } catch (e: Exception) { - logger.error("[TC-${tc.number}] FAILED!! ${e.message}", e) + logger.error("[${tc.number}] FAILED!! ${e.message}", e) tc.result = false return } finally { tc.finishedAt = LocalDateTime.now() } - logger.info("[TC-${tc.number}] SUCCEED!!") + logger.info("[${tc.number}] SUCCEED!!") tc.result = true } private fun startToDumpLog() { - tc.logInfo = LogInfo("${applicationProperties.filePath}/TC-${tc.number}.log") - // TODO 이 함수 호출된 후 main.log 에서 쌓이는 로그를 TC-${tc.number}.log 파일 새로 만들어서 적재 시작 + tc.logInfo = LogInfo("${applicationProperties.fileRoot}/${tc.number}.log") + // TODO 이 함수 호출된 후 main.log 에서 쌓이는 로그를 ${tc.number}.log 파일 새로 만들어서 적재 시작 } private fun stopToDumpLog() { - // TODO main.log >> TC-${tc.number}.log 적재 종료 + // TODO main.log >> ${tc.number}.log 적재 종료 } private fun auth() { @@ -86,7 +87,7 @@ abstract class TestCase { private fun ads() { // TODO: auth - val actualResponse = AdsResponse(0) + val actualResponse = AdsResponse() val response = tc.ads.convert(actualResponse) if (!tc.ads.validate(response)) { throw httpValidationError(response) @@ -115,7 +116,7 @@ abstract class TestCase { } private fun startRecording(tc: Tc) { - tc.recordingInfo = RecordingInfo("${applicationProperties.filePath}/TC-${tc.number}.mp4") + tc.recordingInfo = RecordingInfo("${applicationProperties.fileRoot}/${tc.number}.mp4") logger.debug("start to record. info: {}", tc.recordingInfo) // TODO } diff --git a/src/main/kotlin/tv/anypoint/proxy/adapter/DeviceV3Adapter.kt b/src/main/kotlin/tv/anypoint/proxy/adapter/DeviceV3Adapter.kt new file mode 100644 index 0000000..83916ce --- /dev/null +++ b/src/main/kotlin/tv/anypoint/proxy/adapter/DeviceV3Adapter.kt @@ -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) +} diff --git a/src/main/kotlin/tv/anypoint/proxy/service/NetworkUtil.kt b/src/main/kotlin/tv/anypoint/proxy/service/NetworkUtil.kt new file mode 100644 index 0000000..e0ce2f4 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/proxy/service/NetworkUtil.kt @@ -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 { + val result = mutableListOf() + 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>() + val privateIpList = getPrivateIpV4List() + val available2dArray = Array(privateIpList.size) { arrayOfNulls(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>, Iterable> { + + 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 { + 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.chooseSimilarIp(other: InetAddress) = this.maxByOrNull { it.getIpClassSimilarity(other) } + + fun Enumeration.forEach(block: (T) -> Unit) { + for (i in this) block(i) + } + + fun Enumeration.filter(block: (T) -> Boolean): List { + val result = mutableListOf() + 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()) diff --git a/src/main/kotlin/tv/anypoint/proxy/web/DeviceController.kt b/src/main/kotlin/tv/anypoint/proxy/web/DeviceController.kt new file mode 100644 index 0000000..f80d399 --- /dev/null +++ b/src/main/kotlin/tv/anypoint/proxy/web/DeviceController.kt @@ -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) { + 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() +} diff --git a/src/main/kotlin/tv/anypoint/tc/Base1.kt b/src/main/kotlin/tv/anypoint/tc/Base1.kt index c99f408..efa9d95 100644 --- a/src/main/kotlin/tv/anypoint/tc/Base1.kt +++ b/src/main/kotlin/tv/anypoint/tc/Base1.kt @@ -2,21 +2,22 @@ package tv.anypoint.tc import mu.KLogging import org.springframework.stereotype.Component +import tv.anypoint.domain.adb.ExtraKey +import tv.anypoint.domain.adb.IntentAction import tv.anypoint.dsl.adb +import tv.anypoint.dsl.expected import tv.anypoint.dsl.http +import tv.anypoint.dsl.model.Tc import tv.anypoint.dsl.service.TestCase import tv.anypoint.dsl.tc -import tv.anypoint.dsl.model.Tc -import tv.anypoint.dsl.model.adb.ExtraKey -import tv.anypoint.dsl.model.adb.IntentAction @Component class Base1 : TestCase() { override fun init(): Tc = tc { - number = 1 + number = "BASE-1" reboot = true vast = http { - validate = { false } + validate = { it.id != null } } } @@ -25,8 +26,12 @@ class Base1 : TestCase() { a = IntentAction.CHANGE_TEST_PROPERTY, es = mapOf(ExtraKey.TEST_DEVICE to "true") ) - logger.debug("started to base1 test") - logger.debug("finished to base1 test") + + expected("changed to test device") + + // AD Agent, AD SDK 통신규격상으로는 a 필수, es 는 선택사항 + // https://dev-docs.anypoint.tv/books/ff-settop-ad-agent/page/ad-agent-ad-sdk + // 예외 사항이 존재할 지? adb( a = IntentAction.CHANGE_TEST_PROPERTY, es = mapOf(ExtraKey.TEST_DEVICE to "false") diff --git a/src/main/kotlin/tv/anypoint/tc/Tc1.kt b/src/main/kotlin/tv/anypoint/tc/Tc1.kt index 75b1800..624f0ec 100644 --- a/src/main/kotlin/tv/anypoint/tc/Tc1.kt +++ b/src/main/kotlin/tv/anypoint/tc/Tc1.kt @@ -2,14 +2,14 @@ package tv.anypoint.tc import mu.KLogging import org.springframework.stereotype.Component +import tv.anypoint.dsl.model.Tc import tv.anypoint.dsl.service.TestCase import tv.anypoint.dsl.tc -import tv.anypoint.dsl.model.Tc @Component class Tc1 : TestCase() { override fun init(): Tc = tc { - number = 1 + number = "TC-1" } override fun test() { diff --git a/src/main/kotlin/tv/anypoint/dsl/TestCaseStarter.kt b/src/main/kotlin/tv/anypoint/tc/TestCaseStarter.kt similarity index 76% rename from src/main/kotlin/tv/anypoint/dsl/TestCaseStarter.kt rename to src/main/kotlin/tv/anypoint/tc/TestCaseStarter.kt index 2ffafe2..91a035c 100644 --- a/src/main/kotlin/tv/anypoint/dsl/TestCaseStarter.kt +++ b/src/main/kotlin/tv/anypoint/tc/TestCaseStarter.kt @@ -1,19 +1,18 @@ -package tv.anypoint.dsl +package tv.anypoint.tc import mu.KLogging -import org.springframework.boot.context.event.ApplicationReadyEvent -import org.springframework.context.event.EventListener -import org.springframework.stereotype.Component +import org.springframework.stereotype.Service import tv.anypoint.dsl.service.TestCase -@Component +@Service class TestCaseStarter( private val testCases: List ) { - @EventListener(ApplicationReadyEvent::class) - fun testAfterStartup() { + fun testAll() { // TODO: main.log 덤프 시작 + startDumpLog() + testCases.forEach { it.executeTest() } @@ -33,6 +32,16 @@ class TestCaseStarter( ) ) } + + stopDumpLog() + } + + fun startDumpLog() { + // TODO: + } + + fun stopDumpLog() { + // TODO } companion object : KLogging() diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index d443826..a1c7738 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,9 +1,24 @@ spring: application: name: android-qa + profiles: + active: skb logging: level: root: INFO tv.anypoint: DEBUG anypoint.android-qa: - file-path: /home/bean/dev/qa \ No newline at end of file + 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 diff --git a/src/test/kotlin/tv/anypoint/androidqa/AdsResponseTest.kt b/src/test/kotlin/tv/anypoint/androidqa/AdsResponseTest.kt new file mode 100644 index 0000000..78c070d --- /dev/null +++ b/src/test/kotlin/tv/anypoint/androidqa/AdsResponseTest.kt @@ -0,0 +1,4 @@ +package tv.anypoint.androidqa + +class AdsResponseTest { +} \ No newline at end of file diff --git a/src/test/kotlin/tv/anypoint/androidqa/AndroidQaApplicationTests.kt b/src/test/kotlin/tv/anypoint/androidqa/AndroidQaApplicationTests.kt deleted file mode 100644 index 5dfeafc..0000000 --- a/src/test/kotlin/tv/anypoint/androidqa/AndroidQaApplicationTests.kt +++ /dev/null @@ -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() { - } - -} diff --git a/src/test/kotlin/tv/anypoint/androidqa/AssetConvertResponseTest.kt b/src/test/kotlin/tv/anypoint/androidqa/AssetConvertResponseTest.kt new file mode 100644 index 0000000..19733ba --- /dev/null +++ b/src/test/kotlin/tv/anypoint/androidqa/AssetConvertResponseTest.kt @@ -0,0 +1,4 @@ +package tv.anypoint.androidqa + +class AssetConvertResponseTest { +} \ No newline at end of file diff --git a/src/test/kotlin/tv/anypoint/androidqa/AuthResponseTest.kt b/src/test/kotlin/tv/anypoint/androidqa/AuthResponseTest.kt new file mode 100644 index 0000000..bd844be --- /dev/null +++ b/src/test/kotlin/tv/anypoint/androidqa/AuthResponseTest.kt @@ -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(file.inputStream()) + Then("read values") { + actual.deviceId shouldBe 36986237L + } + } + } + } +} diff --git a/src/test/kotlin/tv/anypoint/androidqa/VastResponseTest.kt b/src/test/kotlin/tv/anypoint/androidqa/VastResponseTest.kt new file mode 100644 index 0000000..9890bc0 --- /dev/null +++ b/src/test/kotlin/tv/anypoint/androidqa/VastResponseTest.kt @@ -0,0 +1,4 @@ +package tv.anypoint.androidqa + +class VastResponseTest { +} \ No newline at end of file diff --git a/src/test/resources/ads-response.json b/src/test/resources/ads-response.json new file mode 100644 index 0000000..26143fa --- /dev/null +++ b/src/test/resources/ads-response.json @@ -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 +} diff --git a/src/test/resources/asset-convert.json b/src/test/resources/asset-convert.json new file mode 100644 index 0000000..869cf03 --- /dev/null +++ b/src/test/resources/asset-convert.json @@ -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" + } +} diff --git a/src/test/resources/auth-response.json b/src/test/resources/auth-response.json new file mode 100644 index 0000000..d402fa4 --- /dev/null +++ b/src/test/resources/auth-response.json @@ -0,0 +1,1580 @@ +{ + "deviceId": 36986237, + "deviceTypeId": 20, + "uuid": "fa6ec0cc-3450-4f1a-bbf8-d6724063f845", + "monitoringInterval": 180000, + "endpoints": { + "auth": "http://192.168.10.13", + "requestAds": "http://192.168.10.13", + "adSyncResult": "http://192.168.10.13", + "appLog": "http://192.168.10.13", + "event": "http://192.168.10.13", + "pushServers": [ + "27.96.131.25:31102" + ], + "stateLog": "http://192.168.10.13", + "impressionLog": "http://192.168.10.13", + "ntpServers": [ + "27.96.134.128", + "49.50.165.198", + "115.85.183.54" + ], + "assetRequest": "http://192.168.10.13", + "proxyAdLog": "http://192.168.10.13" + }, + "channels": [ + { + "id": 10, + "delay": 0, + "serviceId": "503", + "kid": false, + "placementIds": [ + 100, + 110, + 120, + 130 + ], + "testPlacementIds": null + }, + { + "id": 13, + "delay": 0, + "serviceId": "505", + "kid": false, + "placementIds": [ + 130, + 100, + 110, + 120 + ], + "testPlacementIds": null + }, + { + "id": 37, + "delay": 771, + "serviceId": "747", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": [ + 12345 + ] + }, + { + "id": 40, + "delay": 869, + "serviceId": "750", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 43, + "delay": 554, + "serviceId": "749", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 46, + "delay": 2773, + "serviceId": "746", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 49, + "delay": 3539, + "serviceId": "698", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 79, + "delay": -2334, + "serviceId": "616", + "kid": false, + "placementIds": [ + 1, + 2 + ], + "testPlacementIds": null + }, + { + "id": 82, + "delay": -2667, + "serviceId": "771", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 85, + "delay": -163, + "serviceId": "742", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 88, + "delay": -2563, + "serviceId": "604", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 91, + "delay": -328, + "serviceId": "630", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 94, + "delay": -3466, + "serviceId": "627", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 97, + "delay": -4333, + "serviceId": "752", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 100, + "delay": -4400, + "serviceId": "631", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 109, + "delay": 1038, + "serviceId": "617", + "kid": false, + "placementIds": [ + 1, + 2 + ], + "testPlacementIds": null + }, + { + "id": 112, + "delay": 3157, + "serviceId": "612", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": [ + 5 + ] + }, + { + "id": 115, + "delay": 305, + "serviceId": "621", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 118, + "delay": -146, + "serviceId": "626", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 121, + "delay": 769, + "serviceId": "619", + "kid": false, + "placementIds": [ + 2, + 1 + ], + "testPlacementIds": null + }, + { + "id": 124, + "delay": -533, + "serviceId": "712", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 133, + "delay": 0, + "serviceId": "798", + "kid": false, + "placementIds": null, + "testPlacementIds": [ + 1 + ] + }, + { + "id": 136, + "delay": 0, + "serviceId": "797", + "kid": false, + "placementIds": null, + "testPlacementIds": [ + 1 + ] + }, + { + "id": 142, + "delay": -400, + "serviceId": "803", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 145, + "delay": 0, + "serviceId": "605", + "kid": false, + "placementIds": null, + "testPlacementIds": [ + 1 + ] + }, + { + "id": 154, + "delay": 271, + "serviceId": "684", + "kid": false, + "placementIds": [ + 1, + 5 + ], + "testPlacementIds": null + }, + { + "id": 157, + "delay": 2838, + "serviceId": "756", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 160, + "delay": 271, + "serviceId": "685", + "kid": false, + "placementIds": [ + 1, + 5 + ], + "testPlacementIds": null + }, + { + "id": 163, + "delay": 4425, + "serviceId": "654", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 169, + "delay": 505, + "serviceId": "652", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 172, + "delay": 1005, + "serviceId": "671", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 175, + "delay": 3091, + "serviceId": "744", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 181, + "delay": -1201, + "serviceId": "690", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 184, + "delay": 1768, + "serviceId": "666", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 187, + "delay": -1768, + "serviceId": "609", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 190, + "delay": 2725, + "serviceId": "658", + "kid": false, + "placementIds": [ + 1, + 2 + ], + "testPlacementIds": null + }, + { + "id": 193, + "delay": 538, + "serviceId": "725", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 196, + "delay": 106, + "serviceId": "723", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 199, + "delay": -1196, + "serviceId": "700", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 205, + "delay": 2125, + "serviceId": "603", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 208, + "delay": 605, + "serviceId": "696", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 211, + "delay": -697, + "serviceId": "753", + "kid": false, + "placementIds": [ + 1, + 2 + ], + "testPlacementIds": null + }, + { + "id": 214, + "delay": 2101, + "serviceId": "648", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 217, + "delay": 205, + "serviceId": "660", + "kid": false, + "placementIds": [ + 2, + 1 + ], + "testPlacementIds": null + }, + { + "id": 223, + "delay": 0, + "serviceId": "808", + "kid": false, + "placementIds": null, + "testPlacementIds": [ + 1 + ] + }, + { + "id": 232, + "delay": 903, + "serviceId": "683", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 235, + "delay": 4, + "serviceId": "663", + "kid": false, + "placementIds": [ + 1, + 2 + ], + "testPlacementIds": null + }, + { + "id": 238, + "delay": -4233, + "serviceId": "707", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 241, + "delay": 571, + "serviceId": "670", + "kid": false, + "placementIds": [ + 1, + 2 + ], + "testPlacementIds": null + }, + { + "id": 244, + "delay": -1649, + "serviceId": "665", + "kid": false, + "placementIds": [ + 1, + 2 + ], + "testPlacementIds": null + }, + { + "id": 247, + "delay": 3072, + "serviceId": "625", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 250, + "delay": 0, + "serviceId": "641", + "kid": false, + "placementIds": [ + 2, + 1 + ], + "testPlacementIds": null + }, + { + "id": 253, + "delay": 3538, + "serviceId": "620", + "kid": false, + "placementIds": [ + 2, + 1 + ], + "testPlacementIds": null + }, + { + "id": 259, + "delay": 6043, + "serviceId": "691", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 262, + "delay": 0, + "serviceId": "757", + "kid": true, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 271, + "delay": 0, + "serviceId": "435", + "kid": false, + "placementIds": null, + "testPlacementIds": [ + 1 + ] + }, + { + "id": 283, + "delay": -1730, + "serviceId": "175", + "kid": true, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 289, + "delay": 0, + "serviceId": "793", + "kid": true, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 295, + "delay": 2233, + "serviceId": "697", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 298, + "delay": 570, + "serviceId": "755", + "kid": false, + "placementIds": [ + 1 + ], + "taxonomies": null, + "testPlacementIds": null + }, + { + "id": 307, + "delay": -2286, + "serviceId": "795", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 310, + "delay": 3307, + "serviceId": "681", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 316, + "delay": 590, + "serviceId": "615", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": [ + 5 + ] + }, + { + "id": 322, + "delay": 333, + "serviceId": "606", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": [ + 120, + 3000, + 130, + 3100, + 200, + 3200, + 1000, + 3300, + 1100, + 4000, + 1200, + 4100, + 1300, + 4200, + 2000, + 4300, + 2100, + 100, + 2200, + 110, + 2300 + ] + }, + { + "id": 325, + "delay": -1599, + "serviceId": "634", + "kid": false, + "placementIds": [ + 1, + 5 + ], + "testPlacementIds": null + }, + { + "id": 328, + "delay": -4291, + "serviceId": "624", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 331, + "delay": -297, + "serviceId": "688", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 337, + "delay": 672, + "serviceId": "682", + "kid": false, + "placementIds": [ + 1 + ], + "taxonomies": null, + "testPlacementIds": null + }, + { + "id": 340, + "delay": 872, + "serviceId": "687", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": [ + 5 + ] + }, + { + "id": 343, + "delay": 3539, + "serviceId": "613", + "kid": false, + "placementIds": [ + 1, + 2 + ], + "testPlacementIds": null + }, + { + "id": 346, + "delay": 971, + "serviceId": "622", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 349, + "delay": 736, + "serviceId": "618", + "kid": false, + "placementIds": [ + 2, + 1 + ], + "testPlacementIds": null + }, + { + "id": 352, + "delay": 2673, + "serviceId": "637", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 355, + "delay": 204, + "serviceId": "667", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 361, + "delay": 3141, + "serviceId": "668", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 364, + "delay": 2940, + "serviceId": "614", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 367, + "delay": 991, + "serviceId": "602", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 379, + "delay": 304, + "serviceId": "608", + "kid": true, + "placementIds": [ + 2, + 1 + ], + "testPlacementIds": null + }, + { + "id": 382, + "delay": 2671, + "serviceId": "761", + "kid": true, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 385, + "delay": -6303, + "serviceId": "776", + "kid": true, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 388, + "delay": 2704, + "serviceId": "728", + "kid": true, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 397, + "delay": 703, + "serviceId": "693", + "kid": true, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 403, + "delay": 0, + "serviceId": "646", + "kid": false, + "placementIds": [ + 1000, + 3300, + 1100, + 4000, + 1200, + 4100, + 1300, + 4200, + 2000, + 4300, + 2100, + 100, + 2200, + 110, + 2300, + 120, + 3000, + 130, + 3100, + 200, + 3200 + ], + "testPlacementIds": null + }, + { + "id": 409, + "delay": -95, + "serviceId": "636", + "kid": true, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 412, + "delay": -215, + "serviceId": "699", + "kid": true, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 415, + "delay": -2267, + "serviceId": "635", + "kid": true, + "placementIds": [ + 5, + 1 + ], + "testPlacementIds": null + }, + { + "id": 418, + "delay": -4505, + "serviceId": "716", + "kid": true, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 421, + "delay": 834, + "serviceId": "695", + "kid": true, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 424, + "delay": 2252, + "serviceId": "653", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 427, + "delay": -1900, + "serviceId": "623", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 430, + "delay": 5173, + "serviceId": "778", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 433, + "delay": -479, + "serviceId": "644", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 436, + "delay": 271, + "serviceId": "689", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 442, + "delay": 0, + "serviceId": "769", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 469, + "delay": -1900, + "serviceId": "724", + "kid": false, + "placementIds": [ + 1, + 5 + ], + "testPlacementIds": null + }, + { + "id": 472, + "delay": 0, + "serviceId": "655", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 478, + "delay": -674, + "serviceId": "611", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 487, + "delay": 0, + "serviceId": "720", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 499, + "delay": 655, + "serviceId": "686", + "kid": false, + "placementIds": [ + 5, + 1 + ], + "testPlacementIds": null + }, + { + "id": 511, + "delay": 771, + "serviceId": "775", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 512, + "delay": 3340, + "serviceId": "734", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 514, + "delay": 3706, + "serviceId": "694", + "kid": false, + "placementIds": [ + 1, + 2 + ], + "testPlacementIds": null + }, + { + "id": 515, + "delay": -467, + "serviceId": "662", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 524, + "delay": 3539, + "serviceId": "692", + "kid": false, + "placementIds": [ + 1, + 2 + ], + "testPlacementIds": null + }, + { + "id": 536, + "delay": -92, + "serviceId": "656", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 541, + "delay": -128, + "serviceId": "647", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 545, + "delay": 0, + "serviceId": "748", + "kid": false, + "placementIds": [ + 1, + 2 + ], + "testPlacementIds": null + }, + { + "id": 559, + "delay": 2636, + "serviceId": "784", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 687, + "delay": 0, + "serviceId": "582", + "kid": false, + "placementIds": [ + 2 + ], + "testPlacementIds": null + }, + { + "id": 691, + "delay": 0, + "serviceId": "767", + "kid": false, + "placementIds": null, + "testPlacementIds": [ + 3000, + 130, + 3100, + 200, + 3200, + 1000, + 3300, + 1100, + 4000, + 1200, + 4100, + 1300, + 4200, + 2000, + 4300, + 2100, + 100, + 2200, + 110, + 2300, + 120 + ] + }, + { + "id": 698, + "delay": 0, + "serviceId": "741", + "kid": false, + "placementIds": [ + 2 + ], + "testPlacementIds": null + }, + { + "id": 708, + "delay": 0, + "serviceId": "802", + "kid": false, + "placementIds": [ + 2 + ], + "testPlacementIds": null + }, + { + "id": 709, + "delay": 0, + "serviceId": "806", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 723, + "delay": 0, + "serviceId": "575", + "kid": false, + "placementIds": [ + 2 + ], + "testPlacementIds": null + }, + { + "id": 726, + "delay": -2734, + "serviceId": "651", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 728, + "delay": -4266, + "serviceId": "794", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 730, + "delay": -3533, + "serviceId": "726", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 748, + "delay": 0, + "serviceId": "788", + "kid": false, + "placementIds": [ + 1200, + 1, + 3100, + 1300, + 3200, + 2300, + 3300, + 120, + 4000, + 130, + 4100, + 4200, + 4300, + 1000, + 100, + 2000, + 200, + 2200, + 1100, + 3000 + ], + "testPlacementIds": null + }, + { + "id": 749, + "delay": 0, + "serviceId": "789", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 750, + "delay": 0, + "serviceId": "763", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 751, + "delay": -380, + "serviceId": "787", + "kid": false, + "placementIds": [ + 1 + ], + "testPlacementIds": null + }, + { + "id": 862, + "delay": 0, + "serviceId": "804", + "kid": false, + "placementIds": [ + 2 + ], + "testPlacementIds": null + }, + { + "id": 874, + "delay": 0, + "serviceId": "162", + "kid": false, + "placementIds": [ + 2 + ], + "testPlacementIds": null + }, + { + "id": 875, + "delay": 0, + "serviceId": "163", + "kid": false, + "placementIds": [ + 2 + ], + "testPlacementIds": null + }, + { + "id": 891, + "delay": 0, + "serviceId": "569", + "kid": false, + "placementIds": [ + 2 + ], + "testPlacementIds": null + }, + { + "id": 892, + "delay": 0, + "serviceId": "570", + "kid": false, + "placementIds": [ + 2 + ], + "testPlacementIds": null + }, + { + "id": 896, + "delay": 0, + "serviceId": "578", + "kid": false, + "placementIds": [ + 2 + ], + "testPlacementIds": null + }, + { + "id": 901, + "delay": 0, + "serviceId": "564", + "kid": false, + "placementIds": [ + 2 + ], + "testPlacementIds": null + }, + { + "id": 978, + "delay": 2000, + "serviceId": "772", + "kid": false, + "placementIds": [ + 1200, + 1, + 2200, + 3200 + ], + "testPlacementIds": [ + 2 + ] + }, + { + "id": 991, + "delay": 0, + "serviceId": "739", + "kid": false, + "placementIds": [ + 2 + ], + "testPlacementIds": null + }, + { + "id": 994, + "delay": 0, + "serviceId": "800", + "kid": false, + "placementIds": [ + 130, + 100, + 120, + 110 + ], + "testPlacementIds": [ + 1300, + 5 + ] + }, + { + "id": 995, + "delay": 0, + "serviceId": "779", + "kid": false, + "placementIds": null, + "testPlacementIds": [ + 4100, + 1200, + 2200, + 3200, + 1000, + 4200, + 2000, + 1300, + 3000, + 200, + 2300, + 4000, + 3300, + 1100, + 4300, + 2100, + 5000, + 3100, + 2 + ] + }, + { + "id": 996, + "delay": 0, + "serviceId": "799", + "kid": false, + "placementIds": null, + "testPlacementIds": [ + 2200, + 3200, + 2, + 1200 + ] + }, + { + "id": 998, + "delay": 0, + "serviceId": "158", + "kid": true, + "placementIds": null, + "testPlacementIds": [ + 1200, + 2200, + 3200, + 2 + ] + }, + { + "id": 1001, + "delay": 0, + "serviceId": "150", + "kid": false, + "placementIds": null, + "testPlacementIds": [ + 1100, + 1200, + 1300 + ] + } + ], + "adConfig": { + "maxDownloadBandwidth": 300000, + "appPath": "/data/user/0/tv.anypoint.uplus.tvg.app/files/assets/", + "maxUsableStorage": 1048576000, + "minFreeStorage": 130000000, + "trackingRetryInterval": 300000, + "trackingRetryCount": 3, + "remnantTimeThreshold": 2000, + "transitionDelay": 0, + "videoPlayMode": "PARALLEL", + "videoMediaType": "CONCATENATING", + "onPlayDelay": 1, + "startDelay": 132, + "stopDelay": 132, + "startRenderDelay": 0, + "stopRenderDelay": 0, + "maxEndAdPlaytime": 15000, + "overPlayTimeThreshold": 0, + "useLastPositionGoogleAd": true, + "minRequestAdDuration": 5000, + "useStreaming": true, + "useContinuousCue": true, + "maxLazyDownloadBandwidth": 10485760, + "minCueGap" : 2000, + "maxAssignLoop": 20, + "sendBeaconFireResult": true, + "onceAdReadyResponse": false, + "onceAdReadyRequiredTime": 5000, + "onCueCachingTimeWeight": 0.25, + "playerStateCheckParam": { + "playerStateCheckDuration": 7000, + "maxPlayerInvalidateInterval": 3000 + } + }, + "chViewMinTime": 2000, + "kidWatermark": { + "imageUrl": "https://uplus-assets.s3.ap-northeast-2.amazonaws.com/images/watermark.png", + "crc": 2435207897, + "left": 3210, + "top": 66, + "width": 526, + "height": 136 + }, + "accessToken": "16157345-9d93-4cb5-8376-49c789f0d4f3", + "pushSecretKey": "Tx?c)T!3e.52Th5J", + "recognizedSleepTime": 432000000 +} diff --git a/src/test/resources/vast-response.xml b/src/test/resources/vast-response.xml new file mode 100644 index 0000000..6db89b6 --- /dev/null +++ b/src/test/resources/vast-response.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + 5.101 + + + + + + + + + + 00:00:15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"assetId":-50430,"crc":"3113586070","duration":14981,"mediaUrl":"http://123.140.50.75/prod/-50430/ENCODING/18/138961747812860183.mp4","playType":"DNP","bytes":5702218} + + + + + +