import { BrowserPlugin, BrowserTracker, dispatchToTrackersInCollection } from '@snowplow/browser-tracker-core'; import { buildSelfDescribingEvent, resolveDynamicContext, LOG, SelfDescribingJson } from '@snowplow/tracker-core'; import { MediaTracking } from './mediaTracking'; import { MediaPingInterval } from './pingInterval'; import { MediaSessionTracking } from './sessionTracking'; import { CommonMediaEventProperties, MediaEventType, MediaAdBreakType, MediaTrackArguments, MediaTrackAdBreakArguments, MediaTrackAdArguments, MediaTrackingConfiguration, MediaTrackPlaybackRateChangeArguments, MediaEvent, MediaTrackVolumeChangeArguments, MediaTrackFullscreenChangeArguments, MediaTrackPictureInPictureChangeArguments, MediaTrackAdPercentProgressArguments, MediaTrackQualityChangeArguments, MediaTrackErrorArguments, MediaTrackSelfDescribingEventArguments, EventWithContext, } from './types'; export { MediaAdBreakType as MediaPlayerAdBreakType, MediaEventType }; const _trackers: Record = {}; const _context: Record = {}; /** * Adds media tracking */ export function SnowplowMediaPlugin(): BrowserPlugin { let trackerId: string; return { activateBrowserPlugin: (tracker) => { trackerId = tracker.id; _trackers[trackerId] = tracker; _context[trackerId] = []; }, contexts: () => { return _context[trackerId] || []; }, }; } const activeMedias: { [key: string]: MediaTracking } = {}; /** * Starts media tracking for a single media content tracked in a media player. * The tracking instance is uniquely identified by a given ID. * All subsequent media track calls will be processed within this media tracking if given the same ID. * * @param config Configuration for setting up media tracking * @param trackers The tracker identifiers which ping events will be sent to */ export function startMediaTracking( config: MediaTrackingConfiguration & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { const pingInterval = typeof config.pings === 'boolean' ? undefined : config.pings?.pingInterval; const maxPausedPings = typeof config.pings === 'boolean' ? undefined : config.pings?.maxPausedPings; const pings = config.pings === false || config.pings === undefined ? undefined : new MediaPingInterval(pingInterval, maxPausedPings, () => { track({ mediaEvent: { type: MediaEventType.Ping } }, { id: config.id }, trackers); }); const sessionTracking: MediaSessionTracking | undefined = config.session === false ? undefined : new MediaSessionTracking(config.id, config.session?.startedAt, pingInterval); const mediaTracking = new MediaTracking( config.id, config.player, sessionTracking, pings, config.boundaries, config.captureEvents, config.updatePageActivityWhilePlaying, config.filterOutRepeatedEvents, config.context ); activeMedias[mediaTracking.id] = mediaTracking; } /** * Ends media tracking with the given ID if previously started. * Clears local state for the media tracking and sends any events waiting to be sent. * * @param configuration Configuration with the media tracking ID */ export function endMediaTracking(configuration: { id: string }) { if (activeMedias[configuration.id]) { activeMedias[configuration.id].flushAndStop(); delete activeMedias[configuration.id]; } } /** * Updates stored attributes of the media tracking such as the current playback. * Use this function to continually update the player attributes so that they * can be sent in the background ping events. * * @param args The attributes for the media entities * @param trackers The tracker identifiers which any resulting event will be sent to */ export function updateMediaTracking( args: MediaTrackArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track({}, args, trackers); } /** * Tracks a media player ready event that is fired when the media tracking is successfully * attached to the player and can track events. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaReady( args: MediaTrackArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track({ mediaEvent: { type: MediaEventType.Ready } }, args, trackers); } /** * Tracks a media player play event sent when the player changes state to playing from * previously being paused. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaPlay( args: MediaTrackArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track( { mediaEvent: { type: MediaEventType.Play } }, { ...args, player: { ...args.player, paused: false, }, }, trackers ); } /** * Tracks a media player pause event sent when the user pauses the playback. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaPause( args: MediaTrackArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track( { mediaEvent: { type: MediaEventType.Pause } }, { ...args, player: { ...args.player, paused: true, }, }, trackers ); } /** * Tracks a media player end event sent when playback stops when end of the media * is reached or because no further data is available. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaEnd( args: MediaTrackArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track( { mediaEvent: { type: MediaEventType.End } }, { ...args, player: { ...args.player, ended: true, paused: true, }, }, trackers ); } /** * Tracks a media player seek start event sent when a seek operation begins. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaSeekStart( args: MediaTrackArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track({ mediaEvent: { type: MediaEventType.SeekStart } }, args, trackers); } /** * Tracks a media player seek end event sent when a seek operation completes. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaSeekEnd( args: MediaTrackArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track({ mediaEvent: { type: MediaEventType.SeekEnd } }, args, trackers); } /** * Tracks a media player playback rate change event sent when the playback rate has changed. * * If not passed here, the previous rate is taken from the last setting in media player. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaPlaybackRateChange( args: MediaTrackArguments & MediaTrackPlaybackRateChangeArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { const { previousRate, newRate } = args; track( { mediaEvent: { type: MediaEventType.PlaybackRateChange, eventBody: { previousRate: previousRate ?? getMedia(args.id)?.player.playbackRate, newRate, }, }, }, { ...args, player: { ...args.player, playbackRate: newRate }, }, trackers ); } /** * Tracks a media player volume change event sent when the volume has changed. * * If not passed here, the previous volume is taken from the last setting in media player. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaVolumeChange( args: MediaTrackArguments & MediaTrackVolumeChangeArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { const { previousVolume, newVolume } = args; track( { mediaEvent: { type: MediaEventType.VolumeChange, eventBody: { previousVolume: previousVolume ?? getMedia(args.id)?.player.volume, newVolume, }, }, }, { ...args, player: { ...args.player, volume: newVolume }, }, trackers ); } /** * Tracks a media player fullscreen change event fired immediately after * the browser switches into or out of full-screen mode. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaFullscreenChange( args: MediaTrackArguments & MediaTrackFullscreenChangeArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { const { fullscreen } = args; track( { mediaEvent: { type: MediaEventType.FullscreenChange, eventBody: { fullscreen }, }, }, { ...args, player: { ...args.player, fullscreen }, }, trackers ); } /** * Tracks a media player picture-in-picture change event fired immediately * after the browser switches into or out of picture-in-picture mode. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaPictureInPictureChange( args: MediaTrackArguments & MediaTrackPictureInPictureChangeArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { const { pictureInPicture } = args; track( { mediaEvent: { type: MediaEventType.PictureInPictureChange, eventBody: { pictureInPicture }, }, }, { ...args, player: { ...args.player, pictureInPicture }, }, trackers ); } /** * Tracks a media player ad break start event that signals the start of an ad break. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaAdBreakStart( args: MediaTrackArguments & MediaTrackAdBreakArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track({ mediaEvent: { type: MediaEventType.AdBreakStart } }, args, trackers); } /** * Tracks a media player ad break end event that signals the end of an ad break. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaAdBreakEnd( args: MediaTrackArguments & MediaTrackAdBreakArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track({ mediaEvent: { type: MediaEventType.AdBreakEnd } }, args, trackers); } /** * Tracks a media player ad start event that signals the start of an ad. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaAdStart( args: MediaTrackArguments & MediaTrackAdBreakArguments & MediaTrackAdArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track({ mediaEvent: { type: MediaEventType.AdStart } }, args, trackers); } /** * Tracks a media player ad skip event fired when the user activated * a skip control to skip the ad creative. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaAdSkip( args: MediaTrackArguments & MediaTrackAdPercentProgressArguments & MediaTrackAdBreakArguments & MediaTrackAdArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { let { percentProgress } = args; if (percentProgress !== undefined) { percentProgress = Math.floor(percentProgress); } track( { mediaEvent: { type: MediaEventType.AdSkip, eventBody: { percentProgress }, }, }, args, trackers ); } /** * Tracks a media player ad first quartile played event fired when * a quartile of ad is reached after continuous ad playback at normal speed. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaAdFirstQuartile( args: MediaTrackArguments & MediaTrackAdBreakArguments & MediaTrackAdArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track( { mediaEvent: { type: MediaEventType.AdFirstQuartile, eventBody: { percentProgress: 25 }, }, }, args, trackers ); } /** * Tracks a media player ad midpoint played event fired when a midpoint of ad is * reached after continuous ad playback at normal speed. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaAdMidpoint( args: MediaTrackArguments & MediaTrackAdBreakArguments & MediaTrackAdArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track( { mediaEvent: { type: MediaEventType.AdMidpoint, eventBody: { percentProgress: 50 }, }, }, args, trackers ); } /** * Tracks media player ad third quartile played event fired when a quartile * of ad is reached after continuous ad playback at normal speed. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaAdThirdQuartile( args: MediaTrackArguments & MediaTrackAdBreakArguments & MediaTrackAdArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track( { mediaEvent: { type: MediaEventType.AdThirdQuartile, eventBody: { percentProgress: 75 }, }, }, args, trackers ); } /** * Tracks a media player ad complete event that signals the ad creative * was played to the end at normal speed. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaAdComplete( args: MediaTrackArguments & MediaTrackAdBreakArguments & MediaTrackAdArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track({ mediaEvent: { type: MediaEventType.AdComplete } }, args, trackers); } /** * Tracks a media player ad click event fired when the user clicked on the ad. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaAdClick( args: MediaTrackArguments & MediaTrackAdPercentProgressArguments & MediaTrackAdBreakArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { let { percentProgress } = args; if (percentProgress !== undefined) { percentProgress = Math.floor(percentProgress); } track( { mediaEvent: { type: MediaEventType.AdClick, eventBody: { percentProgress }, }, }, args, trackers ); } /** * Tracks a media player ad pause event fired when the user clicked the pause * control and stopped the ad creative. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaAdPause( args: MediaTrackArguments & MediaTrackAdPercentProgressArguments & MediaTrackAdBreakArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { let { percentProgress } = args; if (percentProgress !== undefined) { percentProgress = Math.floor(percentProgress); } track( { mediaEvent: { type: MediaEventType.AdPause, eventBody: { percentProgress }, }, }, args, trackers ); } /** * Tracks a media player ad resume event fired when the user resumed playing the * ad creative after it had been stopped or paused. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaAdResume( args: MediaTrackArguments & MediaTrackAdPercentProgressArguments & MediaTrackAdBreakArguments & MediaTrackAdArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { let { percentProgress } = args; if (percentProgress !== undefined) { percentProgress = Math.floor(percentProgress); } track( { mediaEvent: { type: MediaEventType.AdResume, eventBody: { percentProgress: percentProgress }, }, }, args, trackers ); } /** * Tracks a media player buffering start event fired when the player goes * into the buffering state and begins to buffer content. * * End of buffering can be tracked using `trackMediaBufferEnd` or `trackMediaPlay` or * by updating the `currentTime` property. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaBufferStart( args: MediaTrackArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track({ mediaEvent: { type: MediaEventType.BufferStart } }, args, trackers); } /** * Tracks a media player buffering end event fired when the the player * finishes buffering content and resumes playback. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaBufferEnd( args: MediaTrackArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { track({ mediaEvent: { type: MediaEventType.BufferEnd } }, args, trackers); } /** * Tracks a media player quality change event tracked when the video * playback quality changes automatically. * * If not passed here, the previous quality is taken from the last setting in media player. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaQualityChange( args: MediaTrackArguments & MediaTrackQualityChangeArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { const { previousQuality, newQuality, bitrate, framesPerSecond, automatic, id } = args; track( { mediaEvent: { type: MediaEventType.QualityChange, eventBody: { previousQuality: previousQuality ?? getMedia(id)?.player.quality, newQuality, bitrate, framesPerSecond, automatic, }, }, }, { ...args, player: { ...args.player, quality: newQuality }, }, trackers ); } /** * Tracks a media player error event tracked when the resource could not be loaded due to an error. * * @param args The attributes for the media player event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaError( args: MediaTrackArguments & MediaTrackErrorArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { const { errorCode, errorName, errorDescription } = args; track( { mediaEvent: { type: MediaEventType.Error, eventBody: { errorCode, errorName, errorDescription }, }, }, args, trackers ); } /** * Tracks a custom self-describing event within the context of the media tracking. * It will attach the context entities managed by the media tracking to the event (e.g. player, session, ad). * * @param args The attributes for the event and entities * @param trackers The tracker identifiers which the event will be sent to */ export function trackMediaSelfDescribingEvent( args: MediaTrackSelfDescribingEventArguments & MediaTrackArguments & MediaTrackAdArguments & MediaTrackAdBreakArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { const { event } = args; track({ customEvent: event }, args, trackers); } function getMedia(id: string): MediaTracking | undefined { if (activeMedias[id] === undefined) { LOG.error(`Media tracking ${id} not started.`); return undefined; } return activeMedias[id]; } function track( event: { mediaEvent?: MediaEvent; customEvent?: SelfDescribingJson }, args: MediaTrackArguments & MediaTrackAdArguments & MediaTrackAdBreakArguments & CommonMediaEventProperties, trackers: Array = Object.keys(_trackers) ) { const { context = [], timestamp, player, ad, adBreak, id } = args; const { mediaEvent, customEvent } = event; const mediaTracking = getMedia(id); if (mediaTracking === undefined) { return; } const trackEvent = (event: EventWithContext) => { dispatchToTrackersInCollection(trackers, _trackers, (t) => { t.core.track( buildSelfDescribingEvent(event), (event.context ?? []).concat(resolveDynamicContext(context)), timestamp ); }); }; mediaTracking.update(trackEvent, mediaEvent, customEvent, player, ad, adBreak); // Update page activity in order to keep sending page pings if needed if (mediaTracking.shouldUpdatePageActivity()) { dispatchToTrackersInCollection(trackers, _trackers, (t) => { t.updatePageActivity(); }); } }