동영상 플레이어 설정
AdVideoPlayer 구현
동영상 광고가 재생될 Player 는 SDK 에서 제공하는 것이 아닌 서비스가 사용하는 Player 를 주입받는 형태입니다.
이 과정을 위해서는 서비스의 플레이어가 AdVideoPlayer interface 를 구현해야만 합니다.
(com.naver.gfpsdk.provider.AdVideoPlayer
)
정보
아래 예시는 exo player를 사용하는 예시입니다. 실제 동영상 광고를 적용하실 때는 서비스에서 사용하시는 플레이어로 구현이 필요합니다.
- Kotlin DSL
- Groovy
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")
}
dependencies {
implementation platform('com.naver.gfpsdk:gfpsdk-bom:8.1.0')
implementation 'com.naver.gfpsdk:gfpsdk-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'
}
- Kotlin
- Java
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
}
}
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;
public class SampleExoPlayerView extends StyledPlayerView {
private static final String LOG_TAG = SampleExoPlayerView.class.getSimpleName();
private final CopyOnWriteArraySet<AdVideoPlayer.PlayerCallback> videoPlayerCallbacks =
new CopyOnWriteArraySet<>();
private ExoPlayer player;
private PlaybackState playbackState;
private AdVideoPlayer adPlayer;
private String contentVideoUrl;
private long savedContentPosition = 0L;
private VideoAdMediaFormatChangeListener bitrateChangeListener;
public SampleExoPlayerView(Context context) {
this(context, null);
}
public SampleExoPlayerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SampleExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
player = new ExoPlayer.Builder(context).build();
player.addListener(
new Player.Listener() {
@Override
public void onPlaybackStateChanged(int state) {
// 재생 중 종료 요청이 온 경우에만 complete 처리
if (state == Player.STATE_ENDED && playbackState == PlaybackState.PLAYING) {
adPlayer.disablePlaybackControls();
player.stop();
player.clearMediaItems();
playbackState = PlaybackState.STOPPED;
for (AdVideoPlayer.PlayerCallback callback : videoPlayerCallbacks) {
callback.onCompleted();
}
adPlayer.enablePlaybackControls();
}
}
@Override
public void onPlayerError(@NonNull PlaybackException e) {
Log.e(LOG_TAG, "onPlayerError", e);
for (AdVideoPlayer.PlayerCallback callback : videoPlayerCallbacks) {
callback.onError();
}
}
});
setShowBuffering(SHOW_BUFFERING_WHEN_PLAYING);
setUseController(false);
player.setPlayWhenReady(true);
setPlayer(player);
}
public AdVideoPlayer createAdVideoPlayer(String contentVideoUrl) {
this.contentVideoUrl = contentVideoUrl;
initAdVideoPlayer();
return adPlayer;
}
public void initAdVideoPlayer() {
playbackState = PlaybackState.STOPPED;
adPlayer =
new AdVideoPlayer() {
@Override
public void play() {
if (player != null) {
player.setPlayWhenReady(true);
}
playbackState = PlaybackState.PLAYING;
for (PlayerCallback callback : videoPlayerCallbacks) {
callback.onPlay();
}
}
@Override
public void pause() {
if (player != null) {
player.setPlayWhenReady(false);
}
playbackState = PlaybackState.PAUSED;
for (PlayerCallback callback : videoPlayerCallbacks) {
callback.onPause();
}
}
@Override
public void resume() {
if (player != null) {
player.setPlayWhenReady(true);
}
playbackState = PlaybackState.PLAYING;
for (PlayerCallback callback : videoPlayerCallbacks) {
callback.onResume();
}
}
@Override
public long getCurrentPosition() {
if (player != null) {
return player.getCurrentPosition();
} else {
return 0;
}
}
@Override
public void seekTo(long videoPosition) {
if (player != null) {
player.seekTo(videoPosition);
}
}
@Override
public long getDuration() {
return playbackState == PlaybackState.STOPPED || player == null
? 0
: player.getDuration();
}
@Override
public int getBufferedTime() {
return playbackState == PlaybackState.STOPPED || player == null
? 0
: (int)
(player.getDuration()
* player.getBufferedPercentage()
/ 100);
}
@Override
public float getVolume() {
if (player != null) {
return player.getVolume();
} else {
return 0;
}
}
@Override
public void stopPlayback() {
if (playbackState == PlaybackState.STOPPED) {
return;
}
if (SampleExoPlayerView.this.isPlaying() && player != null) {
player.stop();
}
playbackState = PlaybackState.STOPPED;
}
@Override
public void disablePlaybackControls() {
// nothing
}
@Override
public void enablePlaybackControls() {
// nothing
}
@Override
public void setVideoPath(String videoUrl) {
if (player != null) {
player.setMediaSource(generateMediaSource(videoUrl), true);
player.prepare();
}
}
@Override
public void addPlayerCallback(PlayerCallback callback) {
videoPlayerCallbacks.add(callback);
}
@Override
public void removePlayerCallback(PlayerCallback callback) {
videoPlayerCallbacks.remove(callback);
}
};
adPlayer.enablePlaybackControls();
}
public void pauseContentsRequest() {
savedContentPosition = adPlayer.getCurrentPosition();
adPlayer.stopPlayback();
setUseController(false);
}
public void resumeContentsRequest() {
setUseController(true);
if (player != null) {
player.setMediaSource(generateMediaSource(contentVideoUrl), true);
player.prepare();
adPlayer.seekTo(savedContentPosition);
adPlayer.play();
}
}
private MediaSource generateMediaSource(String videoUrl) {
if (videoUrl != null && videoUrl.toLowerCase().contains(".m3u8")) {
HlsMediaSource hlsMediaSource =
new HlsMediaSource.Factory(
new DefaultDataSource.Factory(
getContext(),
new DefaultHttpDataSource.Factory().setUserAgent("user_agent")))
.createMediaSource(MediaItem.fromUri(videoUrl));
hlsMediaSource.addEventListener(
new Handler(Looper.getMainLooper()),
new MediaSourceEventListener() {
@Override
public void onLoadStarted(
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData) {
// do nothing
}
@Override
public void onLoadCompleted(
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData) {
// do nothing
}
@Override
public void onLoadCanceled(
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData) {
// do nothing
}
@Override
public void onLoadError(
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData,
IOException error,
boolean wasCanceled) {
// do nothing
}
@Override
public void onUpstreamDiscarded(
int windowIndex,
MediaSource.MediaPeriodId mediaPeriodId,
MediaLoadData mediaLoadData) {
// do nothing
}
@Override
public void onDownstreamFormatChanged(
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
MediaLoadData mediaLoadData) {
if (bitrateChangeListener != null && mediaLoadData.trackFormat != null) {
bitrateChangeListener.onMediaFormatChanged(
mediaLoadData.trackFormat.bitrate / 1000,
mediaLoadData.trackFormat.containerMimeType);
}
}
});
return hlsMediaSource;
} else {
return new ProgressiveMediaSource.Factory(
new DefaultDataSource.Factory(
getContext(), new DefaultHttpDataSource.Factory().setUserAgent("user_agent")))
.createMediaSource(MediaItem.fromUri(videoUrl));
}
}
/** player reset */
public void reset() {
savedContentPosition = 0;
if (player != null) {
player.stop();
player.clearMediaItems();
}
}
/** playerView & player release */
public void release() {
if (player != null) {
player.release();
player = null;
}
adPlayer = null;
videoPlayerCallbacks.clear();
removeAllViews();
}
public boolean isPlaying() {
return playbackState == PlaybackState.PLAYING;
}
public boolean isPaused() {
return playbackState == PlaybackState.PAUSED;
}
public void addAdBitrateChangeListener(VideoAdMediaFormatChangeListener listener) {
this.bitrateChangeListener = listener;
}
public interface VideoAdMediaFormatChangeListener {
void onMediaFormatChanged(int bitrate, String mimeType);
}
public enum PlaybackState {
STOPPED,
PAUSED,
PLAYING
}
}