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) { |
||||
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) { |
||||
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 +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: |
||||
application: |
||||
name: android-qa |
||||
profiles: |
||||
active: skb |
||||
logging: |
||||
level: |
||||
root: INFO |
||||
tv.anypoint: DEBUG |
||||
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