본문으로 건너뛰기

동영상 플레이어 설정

AdVideoPlayer 구현

동영상 광고가 재생될 Player 는 SDK 에서 제공하는 것이 아닌 서비스가 사용하는 Player 를 주입받는 형태입니다.

이 과정을 위해서는 서비스의 플레이어가 AdVideoPlayer interface 를 구현해야만 합니다. (com.naver.gfpsdk.provider.AdVideoPlayer)

정보

아래 예시는 exo player를 사용하는 예시입니다. 실제 동영상 광고를 적용하실 때는 서비스에서 사용하시는 플레이어로 구현이 필요합니다.

dependencies {
implementation(platform("com.naver.gfpsdk:nam-bom:8.1.0"))
implementation("com.naver.gfpsdk:nam-core")

implementation("com.google.android.exoplayer:exoplayer-core:2.17.0")
implementation("com.google.android.exoplayer:exoplayer-hls:2.17.0")
implementation("com.google.android.exoplayer:exoplayer-ui:2.17.0")
}
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.util.Log

import androidx.annotation.NonNull
import androidx.annotation.Nullable
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.source.LoadEventInfo
import com.google.android.exoplayer2.source.MediaLoadData
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.MediaSourceEventListener
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.naver.gfpsdk.provider.AdVideoPlayer
import java.io.IOException
import java.util.concurrent.CopyOnWriteArraySet

class SampleExoPlayerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : StyledPlayerView(context, attrs, defStyleAttr) {

private val LOG_TAG = SampleExoPlayerView::class.java.simpleName
private val videoPlayerCallbacks = CopyOnWriteArraySet<AdVideoPlayer.PlayerCallback>()
private var player: ExoPlayer
private var playbackState: PlaybackState
private var adPlayer: AdVideoPlayer
private var contentVideoUrl: String? = null
private var savedContentPosition = 0L
private var bitrateChangeListener: VideoAdMediaFormatChangeListener? = null

init {
player = ExoPlayer.Builder(context).build()
player.addListener(
object: Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_ENDED && playbackState == PlaybackState.PLAYING) {
adPlayer.disablePlaybackControls()
player.stop()
player.clearMediaItems()
playbackState = PlaybackState.STOPPED

videoPlayerCallbacks.forEach { it.onCompleted() }

adPlayer.enablePlaybackControls()
}
}

override fun onPlayerError(e: PlaybackException) {
Log.e(LOG_TAG, "onPlayerError", e)

videoPlayerCallbacks.forEach { it.onError() }
}
}
)

setShowBuffering(SHOW_BUFFERING_WHEN_PLAYING)
useController = false
player.playWhenReady = true
setPlayer(player)

playbackState = PlaybackState.STOPPED

adPlayer = object: AdVideoPlayer {
override fun play() {
player.playWhenReady = true
playbackState = PlaybackState.PLAYING

videoPlayerCallbacks.forEach { it.onPlay() }
}

override fun pause() {
player.playWhenReady = false
playbackState = PlaybackState.PAUSED

videoPlayerCallbacks.forEach { it.onPause() }
}

override fun resume() {
player.playWhenReady = true
playbackState = PlaybackState.PLAYING

videoPlayerCallbacks.forEach { it.onResume() }
}

override fun getCurrentPosition(): Long {
return player.currentPosition
}

override fun seekTo(videoPosition: Long) {
player.seekTo(videoPosition)
}

override fun getDuration(): Long {
return if (playbackState == PlaybackState.STOPPED || player == null) 0L else player.duration
}

override fun getBufferedTime(): Int {
return if (playbackState == PlaybackState.STOPPED || player == null) 0 else
(player.duration * player.bufferedPercentage / 100).toInt()
}

override fun getVolume(): Float {
return player.volume
}

override fun stopPlayback() {
if (playbackState == PlaybackState.STOPPED) {
return
}

if (isPlaying && player != null) {
player.stop()
}

playbackState = PlaybackState.STOPPED
}

override fun disablePlaybackControls() {
// nothing
}

override fun enablePlaybackControls() {
// nothing
}

override fun setVideoPath(videoUrl: String) {
player.setMediaSource(generateMediaSource(videoUrl), true)
player.prepare()
}

override fun addPlayerCallback(callback: AdVideoPlayer.PlayerCallback) {
videoPlayerCallbacks.add(callback)
}

override fun removePlayerCallback(callback: AdVideoPlayer.PlayerCallback) {
videoPlayerCallbacks.remove(callback)
}
}
adPlayer.enablePlaybackControls()
}

fun createAdVideoPlayer(contentVideoUrl: String): AdVideoPlayer {
this.contentVideoUrl = contentVideoUrl
initAdVideoPlayer()
return adPlayer
}

fun initAdVideoPlayer() {
playbackState = PlaybackState.STOPPED
adPlayer = object: AdVideoPlayer {
override fun play() {
player?.let { it.playWhenReady = true }

playbackState = PlaybackState.PLAYING

videoPlayerCallbacks.forEach { it.onPlay() }
}

override fun pause() {
player?.let { it.playWhenReady = false }

playbackState = PlaybackState.PAUSED

videoPlayerCallbacks.forEach { it.onPause() }
}

override fun resume() {
player?.let { it.playWhenReady = true }

playbackState = PlaybackState.PLAYING

videoPlayerCallbacks.forEach { it.onResume() }
}

override fun getCurrentPosition(): Long {
return if (player != null) {
player.currentPosition
} else {
0
}
}

override fun seekTo(videoPosition: Long) {
player?.let { it.seekTo(videoPosition) }
}

override fun getDuration(): Long {
return if (playbackState == PlaybackState.STOPPED || player == null) {
0
} else {
player.duration
}
}

override fun getBufferedTime(): Int {
return if (playbackState == PlaybackState.STOPPED || player == null) {
0
} else {
(player.duration * player.bufferedPercentage / 100).toInt()
}
}

override fun getVolume(): Float {
return if (player != null) {
player.volume
} else {
0f
}
}

override fun stopPlayback() {
if (playbackState == PlaybackState.STOPPED) {
return
}

if (isPlaying && player != null) {
player.stop()
}

playbackState = PlaybackState.STOPPED
}

override fun disablePlaybackControls() {
// nothing
}

override fun enablePlaybackControls() {
// nothing
}

override fun setVideoPath(videoUrl: String) {
player?.let {
it.setMediaSource(generateMediaSource(videoUrl), true)
it.prepare()
}
}

override fun addPlayerCallback(callback: AdVideoPlayer.PlayerCallback) {
videoPlayerCallbacks.add(callback)
}

override fun removePlayerCallback(callback: AdVideoPlayer.PlayerCallback) {
videoPlayerCallbacks.remove(callback)
}
}
adPlayer.enablePlaybackControls()
}

fun pauseContentsRequest() {
savedContentPosition = adPlayer.currentPosition
adPlayer.stopPlayback()
useController = false
}

fun resumeContentsRequest() {
useController = true
player?.let {
it.setMediaSource(generatedMediaSource(contentVideoUrl), true)
it.prepare()
adPlayer.seekTo(savedContentPosition)
adPlayer.play()
}
}

private fun generateMediaSource(videoUrl: String?): MediaSource {
return if (videoUrl != null && videoUrl.toLowerCase().contains(".m3u8")) {
val hlsMediaSource = HlsMediaSource.Factory(
DefaultDataSource.Factory(
context,
DefaultHttpDataSource.Factory().setUserAgent("user_agent")
)
).createMediaSource(MediaItem.fromUri(videoUrl))

hlsMediaSource.apply {
addEventListener(
Handler(Looper.getMainLooper()),
object: MediaSourceEventListener {
override fun onLoadStarted(
windowIndex: Int,
mediaPeriodId: MediaSource.MediaPeriodId?,
loadEventInfo: LoadEventInfo,
mediaLoadData: MediaLoadData
) {
// do nothing
}

override fun onLoadCompleted(
windowIndex: Int,
mediaPeriodId: MediaSource.MediaPeriodId?,
loadEventInfo: LoadEventInfo,
mediaLoadData: MediaLoadData
) {
// do nothing
}

override fun onLoadCanceled(
windowIndex: Int,
mediaPeriodId: MediaSource.MediaPeriodId?,
loadEventInfo: LoadEventInfo,
mediaLoadData: MediaLoadData
) {
// do nothing
}

override fun onLoadError(
windowIndex: Int,
mediaPeriodId: MediaSource.MediaPeriodId?,
loadEventInfo: LoadEventInfo,
mediaLoadData: MediaLoadData,
error: IOException,
wasCanceled: Boolean
) {
// do nothing
}

override fun onUpstreamDiscarded(
windowIndex: Int,
mediaPeriodId: MediaSource.MediaPeriodId,
mediaLoadData: MediaLoadData
) {
// do nothing
}

override fun onDownstreamFormatChanged(
windowIndex: Int,
mediaPeriodId: MediaSource.MediaPeriodId?,
mediaLoadData: MediaLoadData
) {
if (bitrateChangeListener != null && mediaLoadData.trackFormat != null) {
bitrateChangeListener?.onMediaFormatChanged(
mediaLoadData.trackFormat.bitrate / 1000,
mediaLoadData.trackFormat.containerMimeType
)
}
}
}
)
}
} else {
ProgressiveMediaSource.Factory(
DefaultDataSource.Factory(
context,
DefaultHttpDataSource.Factory().setUserAgent("user_agent")
)
).createMediaSource(MediaItem.fromUri(videoUrl))
}
}

fun reset() {
savedContentPosition = 0

player?.let {
it.stop()
it.clearMediaItems()
}
}

fun release() {
if (player != null) {
player.release()
player = null
}

adPlayer = null
videoPlayerCallbacks.clear()
removeAllViews()
}

val isPlaying: Boolean
get() = playbackState == PlaybackState.PLAYING

val isPaused: Boolean
get() = playbackState == PlaybackState.PAUSED

fun addAdBitrateChangeListener(listener: VideoAdMediaFormatChangeListener) {
bitrateChangeListener = listener
}

interface VideoAdMediaFormatChangeListener {
fun onMediaFormatChanged(bitrate: Int, mimeType: String)
}

enum class PlaybackState {
STOPPED, PAUSED, PLAYING
}
}