googleads / videojs-ima

IMA SDK Plugin for Video.js
Apache License 2.0
445 stars 285 forks source link

Why mid-roll ad not working when changing playback speed from 1 to 2? #1116

Open ruby-duongtv opened 11 months ago

ruby-duongtv commented 11 months ago

Can you explain to me why videojs is not playing ads when set video playback from 1 to 2? Thank for your help.

Kiro705 commented 11 months ago

Hello @t-duong ,

Would you be able to share a code snippet or sample project where this change is being done? I think that would help be in debugging any issue you are experiencing.

Thank you, Jackson IMA SDK team

ruby-duongtv commented 11 months ago

@Kiro705 Thank you for answering my question. Here is my code.

<template lang="pug">
div(:class="$style.player_wrapper")
  video.video-js.theme-custom(
    ref="player"
    :class="$style.player"
  )
</template>

<script>
import videojs from 'video.js'
import 'video.js/dist/video-js.css'
import * as contribAds from 'videojs-contrib-ads'
import 'videojs-ima'
import 'videojs-ima/dist/videojs.ima.css'
import * as contribQualityLevels from 'videojs-contrib-quality-levels'

import './MediaText'
import './LoadingSpinner'
import './PlaybackRateMenuButton'
import './BigPlayToggle'
import './EndedNextMedia'
import './HlsQualitySelector'
import './SeekPreview'

videojs.registerPlugin('ads', contribAds.default)
videojs.registerPlugin('qualityLevels', contribQualityLevels.default)

export default {
  props: {
    media: { type: Object, required: true },
    nextMedia: { type: Object, default: null },
    isAutoplay: { type: Boolean, default: true },
    isMux: { type: Boolean, default: false },
    isLog: { type: Boolean, default: true },
    nextDelay: { type: Number, default: null },
    nextAutoplay: { type: Boolean, default: false },
    isAdminMode: { type: Boolean, default: false }
  },

  data: () => ({
    player: null,
    playlistId: null,
    initialized: false,
    options: {
      html5: {
        vhs: {
          overrideNative: !videojs.browser.IS_IPHONE
        }
      },
      language: 'ja',
      aspectRatio: '16:9',
      fill: true,
      fluid: false,
      controls: true,
      liveui: true,
      poster: null,
      autoplay: false,
      muted: false,
      playsinline: true,
      playbackRates: [1, 2],
      techOrder: ['html5'],
      enableSourceset: true,
      textTrackSettings: false,
      mediaText: {
        title: '',
        description: ''
      },
      bigPlayToggle: true,
      endedNextMedia: {
        autoplay: false,
        delay: 10,
        media: null
      },
      controlBar: {
        volumePanel: { inline: false },
        progressControl: {
          seekBar: {
            seekPreview: {
              spriteUrlPrefix: null,
              spriteUrlQuery: null
            }
          }
        },
        children: [
          // 'playToggle',
          'currentTimeDisplay',
          'timeDivider',
          'durationDisplay',
          'progressControl',
          'liveDisplay',
          'seekToLive',
          'remainingTimeDisplay',
          'customControlSpacer',
          'PlaybackRateMenuButton',
          // 'chaptersButton',
          // 'descriptionsButton',
          // 'subsCapsButton',
          // 'audioTrackButton',
          // 'pictureInPictureToggle',
          'volumePanel',
          'HlsQualitySelector',
          'fullscreenToggle'
        ]
      }
    },
    needsPlayingAfterAdEnd: true // Postroll時のみfalseにする
  }),

  watch: {
    media(newMedia, oldMedia) {
      if (!newMedia && !oldMedia) return false
      if (newMedia.id !== oldMedia.id) {
        this.reset()
        this.init()
      }
    }
  },

  mounted() {
    // 異なるレイアウト(検索結果一覧のempty)から遷移してくると2重にマウントされてしまう問題の対応
    // https://github.com/nuxt/nuxt.js/issues/5703
    // 2重でマウントされるとプレイヤーが2つ動いてしまい、1つは制御不能で再生され続けてしまう
    // Nuxt.jsコントロール配下でない場合(embed.jsなど)もあるので注意する
    if (this.$nuxt) {
      if (this.validLayoutName()) {
        this.init()
      }
    } else {
      this.init()
    }
  },

  beforeDestroy() {
    this.destroy()
  },

  methods: {
    validLayoutName() {
      return this.$nuxt.layoutName === 'default'
    },

    async init() {
      const { player } = this.$refs
      if (this.isAdminMode) {
        if (!this.media.video.live_hls_for_admin) {
          console.error('live_hls_for_admin not found')
          return
        }
      } else if (!this.media.video.hls) {
        console.error('hls not found')
        return
      }
      let videoSrc
      if (this.isAdminMode) {
        videoSrc = this.media.video.live_hls_for_admin || ''
      } else {
        videoSrc = this.media.video.hls
      }
      videoSrc = videoSrc
        .replace('%%device_type%%', 'web')
        .replace('%%page%%', location.href)
        .replace('%%uuid%%', '')

      const hlsKeyQuery = this.media.video.hls_key_query
      const appendDelimiter = videoSrc.includes('?') ? '&' : '?'
      const videoSrcWithToken =
        videojs.browser.IS_IPHONE && hlsKeyQuery
          ? videoSrc + appendDelimiter + hlsKeyQuery
          : videoSrc

      const video = {
        type: 'application/x-mpegURL',
        // src: 'https://d2zihajmogu5jn.cloudfront.net/elephantsdream/hls/ed_hd.m3u8',
        // src: 'https://d2zihajmogu5jn.cloudfront.net/big-buck-bunny/master.m3u8',
        // src: 'https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8',
        src: videoSrcWithToken,
        withCredentials: false
      }

      // - 埋込プレイヤーでは $route が存在しない
      // - 緊急ライブでは media.playlist が存在しない
      if (this.$route && this.media.playlist) {
        const { query } = this.$route
        this.playlistId =
          query.list !== undefined ? query.list : this.media.playlist.id
      }

      if (this.nextDelay) {
        this.options.endedNextMedia.delay = this.nextDelay
      }
      this.options.endedNextMedia.autoplay = this.nextAutoplay

      // can autoplay
      const autoplaySupport = await this.$canAutoplay.checkUnmutedAutoplaySupport()
      if (this.isAutoplay && autoplaySupport.autoplayAllowed) {
        // autoplay=true では、回線Fast3G + 広告有り 状態の場合に
        // 広告後に本編がautoplayされない場合がある為、autoplay='any'にする
        this.options.autoplay = 'any'
        this.options.muted = autoplaySupport.autoplayRequiresMute
      }

      if (!this.canTimeShiftedViewing()) {
        delete this.options.controlBar.progressControl
        const i = this.options.controlBar.children.indexOf('progressControl')
        this.options.controlBar.children.splice(i, 1)
      }

      this.player = videojs(player, this.options, () => {
        // this.player.volume(1)
      })
      this.player.on('nextplay', this._onNextPlay)
      this.player.on('timeupdate', this._onTimeupdate)
      this.player.one('loadedmetadata', this._onLoadedmetadata)
      this.player.on('play', this._onPlay)
      this.player.on('pause', this._onPause)
      this.player.on('ended', this._onEnded)
      this.player.on('error', this._onError)

      const tech = this.player.tech({ IWillNotUseThisInPlugins: true })
      tech.on('retryplaylist', this._onRetryplaylist)

      // for encrypted hls
      videojs.Vhs.xhr.beforeRequest = function(options) {
        // ex: dev-license.locipo.jp/keys/*** / licence.locipo.jp/keys/***
        if (options.uri.includes('licence.locipo.jp/keys/')) {
          const appendDelimiter = options.uri.includes('?') ? '&' : '?'
          options.uri += hlsKeyQuery ? appendDelimiter + hlsKeyQuery : ''
        }
        return options
      }

      // components setup
      this.player.poster(this.media.thumb)
      this.player.mediaText.options({
        title: this.media.title,
        description: this.media.description
      })
      if (this.canTimeShiftedViewing()) {
        this.player.controlBar.progressControl.seekBar.seekPreview.options({
          spriteUrlPrefix: this.media.video.seek_preview_url_prefix,
          spriteUrlQuery: this.media.video.seek_preview_url_query
        })
      }
      this.player.endedNextMedia.options({
        media: this.nextMedia
      })

      // tracker setup
      const ga = this.$ga
        .set('mediaId', this.media.id)
        .set('mediaTitle', this.media.title)
        .set('stationId', this.media.station_id)
        .set('stationCd', this.media.station_cd)
        .set('firstPlay', true)

      ga.playerWatcherStart()

      if (this.media.playlist) {
        this.$ga
          .set('seriesUuid', this.media.playlist.id)
          .set('seriesName', this.media.playlist.title)
      }

      // add ima setup
      if (this.media.video.ad_uri) {
        const adTagUrl = this.media.video.ad_uri
          .replace('%7Bdevice%7D', this._isMobile() ? 'sp_web' : 'web')
          .replace('%7Breferrer_url%7D', location.href)
          .replace('%7Buuid%7D', '')
          .replace('%7Bidtype%7D', '')

        if (!this.player.ima.changeAdTag) {
          const imaOptions = {
            // debug: true,
            locale: 'ja',
            adLabel: '広告',
            adLabelNofN: '/',
            // adTagUrl: 'https://search.spotxchange.com/vmap/1.0/207470?adPlaylistId=4070&channelId=219765?VPI[]=MP4&player_width=640&player_height=360&content_page_url=https%3A%2F%2Fwww.cci.co.jp%2F&custom[genre]=drama&custom[program]=test&custom[episode]=001'
            // adTagUrl: 'https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator='
            adTagUrl,
            contribAdsSettings: {
              // adtimeout発生後にadsreadyが処理されて一時停止状態が解除されなくなる問題があるため、timeoutと同じ時間を設定しておく
              prerollTimeout: 5000
            },
            disableCustomPlaybackForIOS10Plus: true
          }
          this.player.on('adsready', this._onAdsready)
          this.player.on('adend', this._onAdEnd)
          this.player.ima(imaOptions)
        } else {
          this.player.ima.changeAdTag(adTagUrl)
          this.player.ima.requestAds()
        }
      }

      this.player.src(video)
      if (this.initialized && this.player.paused()) {
        this.player.play()
      }
      this.initialized = true
    },

    reset() {
      if (this.player) {
        this.player.pause()
        this.player.off('nextplay', this._onNextPlay)
        this.player.off('timeupdate', this._onTimeupdate)
        this.player.off('play', this._onPlay)
        this.player.off('pause', this._onPause)
        this.player.off('ended', this._onEnded)
        this.player.off('error', this._onError)
        this.player.off('adsready', this._onAdsready)
        this.player.off('adend', this._onAdEnd)
        this.player.reset()
      }

      this.$ga.playerWatcherDestroy()
    },

    destroy() {
      this.reset()

      if (this.player) {
        this.player.dispose()
        this.player = null
      }
    },

    canTimeShiftedViewing() {
      if (this.media.creative_type === 'live_stream') {
        return this.media.time_shifted_viewing
      } else {
        return true
      }
    },

    seekToLiveEdge() {
      this.player.currentTime(this.player.liveTracker.liveCurrentTime())
    },

    _onNextPlay(event) {
      this.$emit('nextplay')
    },

    _onError(event) {
      this.$emit('error', event)
    },

    _onRetryplaylist(event) {
      this.$emit('retryplaylist', event)
    },

    _onTimeupdate(event) {
      const ct = this.player.currentTime()
      const duration = this.player.duration()

      this.$ga.set('currentTime', ct)

      if (this.isLog && this.$store && duration !== 'Infinity') {
        this.$store.commit('log/addLog', {
          media: this.media,
          duration: duration * 1000, // milliseconds
          currentPosition: ct * 1000, // milliseconds
          playlistId: this.playlistId,
          updateAt: Date.now()
        })
      }
    },

    _onLoadedmetadata(event) {
      if (this.isLog && this.$store) {
        const log = this.$store.getters['log/getLog'](this.media)
        if (log) {
          // 保存しているcurrentPositionが動画終了時間から6秒以内だったら最初から再生させる
          const marginSecond = 6
          const logPos = log.currentPosition / 1000
          const duration = this.player.duration()
          const startPos = duration > logPos + marginSecond ? logPos : 0
          this.player.currentTime(startPos)
        }

        if (this._needsCurrentLive()) {
          this.seekToLiveEdge()
        }
      }

      this.$ga.set('currentTime', this.player.currentTime())
    },

    _onPlay(event) {
      this.$ga.sendPlayEvent()
      this.$ga.set('firstPlay', false)

      if (this._needsCurrentLive()) {
        this.seekToLiveEdge()
      }
    },

    _onPause(event) {
      this.$ga.sendPauseEvent()
    },

    _onEnded(event) {
      this.$ga.sendEndedEvent()
    },

    _onAdsready() {
      const { STARTED } = window.google.ima.AdEvent.Type
      this.player.ima.addEventListener(STARTED, this._onAdsStarted)
      this.player.ads.startLinearAdMode()
    },

    _onAdsStarted() {
      // ライブ時の広告終了後に再生を再開させるかどうかのフラグを設定
      // readyforpreroll,readyforpostrollイベントをフックして設定しても良いが、
      // midrollにも対応させるためここで設定しておく
      // postroll後のみ再開させない
      if (this.player.ads.adType === 'postroll') {
        this.needsPlayingAfterAdEnd = false
      } else {
        this.needsPlayingAfterAdEnd = true
      }

      // Preroll Ad時のみ一時停止イベントが発火しないので発火させる
      this.$ga.sendPauseEvent()
    },

    _onAdEnd() {
      if (
        this.initialized &&
        this.player.paused() &&
        this.needsPlayingAfterAdEnd
      ) {
        // ライブでの広告再生後に再開されないため再開しておく
        this.player.play()
      }
    },

    _needsCurrentLive() {
      if (this.media.creative_type === 'live_stream' && this.isAdminMode) {
        return true
      }

      if (!this.canTimeShiftedViewing()) {
        return true
      }

      return false
    },

    _isMobile() {
      const mobileKey = ['mobile', 'android', 'iphone', 'ipad', 'ipod']
      return mobileKey.some((keyword) =>
        navigator.userAgent.toLowerCase().includes(keyword)
      )
    }
  }
}
</script>
ruby-duongtv commented 10 months ago

@Kiro705 Sorry, I can't share all the sources of my project. Can you help me?

Kiro705 commented 10 months ago

Hello @t-duong ,

Thank you for sharing the code snippet, I was able to see the issue by adding the following line to the plugin's sample app:

 playbackRates: [1, 2],

I was able to see that ads are not played at x2 speed. Testing on a HTML5 basic

I can plan to look into a fix, but right a work-around would be to use a different player.

Thank you, Jackson IMA SDK team

ruby-duongtv commented 10 months ago

@Kiro705 Thank for your support!

dioramayuanito commented 8 months ago

are there any workarounds to solve this case?

Decoydoll commented 8 months ago

Hello @Kiro705 ,

Is there any update about this? Thank you!

dioramayuanito commented 8 months ago

My temporary solution is : https://www.youtube.com/watch?v=w67wSxbyQTw

I get all cue-points from

var cuePoints = player.ima.getAdsManager().getCuePoints();

and if currentTime in timeupdate event 2 secs near/before of each cues, then i change speed to 1x