Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ruby-duongtv opened this issue Aug 17, 2023 · 8 comments
Open
Assignees

Comments

@ruby-duongtv
Copy link

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

@Kiro705 Kiro705 self-assigned this Aug 17, 2023
@Kiro705
Copy link
Member

Kiro705 commented Aug 17, 2023

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
Copy link
Author

ruby-duongtv commented Aug 18, 2023

@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
Copy link
Author

ruby-duongtv commented Aug 30, 2023

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

@Kiro705
Copy link
Member

Kiro705 commented Aug 30, 2023

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
Copy link
Author

@Kiro705
Thank for your support!

@dioramayuanito
Copy link

are there any workarounds to solve this case?

@Decoydoll
Copy link

Hello @Kiro705 ,

Is there any update about this? Thank you!

@dioramayuanito
Copy link

dioramayuanito commented Nov 27, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants