diff --git a/frontend/src/scenes/session-recordings/player/rrweb/index.ts b/frontend/src/scenes/session-recordings/player/rrweb/index.ts index 9df4a506936..97a3c1eddb2 100644 --- a/frontend/src/scenes/session-recordings/player/rrweb/index.ts +++ b/frontend/src/scenes/session-recordings/player/rrweb/index.ts @@ -1,3 +1,4 @@ +import Hls from 'hls.js' import { EventType, eventWithTime, IncrementalSource } from 'rrweb' import { playerConfig, ReplayPlugin } from 'rrweb/typings/types' @@ -122,6 +123,44 @@ export const WindowTitlePlugin = (cb: (windowId: string, title: string) => void) } } +export const HLSPlayerPlugin: ReplayPlugin = { + onBuild: (node) => { + if (node && node.nodeName === 'VIDEO' && node.nodeType === 1) { + const videoEl = node as HTMLVideoElement + const hlsSrc = videoEl.getAttribute('hls-src') + + if (videoEl && hlsSrc) { + if (Hls.isSupported()) { + const hls = new Hls() + hls.loadSource(hlsSrc) + hls.attachMedia(videoEl) + + hls.on(Hls.Events.ERROR, (_, data) => { + if (data.fatal) { + switch (data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: + hls.startLoad() + break + case Hls.ErrorTypes.MEDIA_ERROR: + hls.recoverMediaError() + break + // Unrecoverable error + default: + hls.destroy() + break + } + } + }) + } + // HLS not supported natively but can play in Safari + else if (videoEl.canPlayType('application/vnd.apple.mpegurl')) { + videoEl.src = hlsSrc + } + } + } + }, +} + const defaultStyleRules = `.ph-no-capture { background-image: ${PLACEHOLDER_SVG_DATA_IMAGE_URL} }` // replaces a common rule in Shopify templates removed during capture // fix tracked in https://github.com/rrweb-io/rrweb/pull/1322 diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts index 5ed37a43c0d..180b6945a35 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts @@ -41,7 +41,7 @@ import { AvailableFeature, RecordingSegment, SessionPlayerData, SessionPlayerSta import type { sessionRecordingsPlaylistLogicType } from '../playlist/sessionRecordingsPlaylistLogicType' import { playerSettingsLogic } from './playerSettingsLogic' -import { COMMON_REPLAYER_CONFIG, CorsPlugin } from './rrweb' +import { COMMON_REPLAYER_CONFIG, CorsPlugin, HLSPlayerPlugin } from './rrweb' import { CanvasReplayerPlugin } from './rrweb/canvas/canvas-plugin' import type { sessionRecordingPlayerLogicType } from './sessionRecordingPlayerLogicType' import { deleteRecording } from './utils/playerUtils' @@ -596,7 +596,7 @@ export const sessionRecordingPlayerLogic = kea( return } - const plugins: ReplayPlugin[] = [] + const plugins: ReplayPlugin[] = [HLSPlayerPlugin] // We don't want non-cloud products to talk to our proxy as it likely won't work, but we _do_ want local testing to work if (values.preflight?.cloud || window.location.hostname === 'localhost') { diff --git a/package.json b/package.json index 3814c9102c5..4546ad15c30 100644 --- a/package.json +++ b/package.json @@ -140,6 +140,7 @@ "kea-test-utils": "^0.2.4", "kea-waitfor": "^0.2.1", "kea-window-values": "^3.0.0", + "hls.js": "^1.5.15", "lodash.merge": "^4.6.2", "maplibre-gl": "^3.5.1", "md5": "^2.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e41c20d91c7..efbf595d967 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,6 +208,9 @@ dependencies: heatmap.js: specifier: ^2.0.5 version: 2.0.5(patch_hash=gydrxrztd4ruyhouu6tu7zh43e) + hls.js: + specifier: ^1.5.15 + version: 1.5.15 husky: specifier: ^7.0.4 version: 7.0.4 @@ -13538,6 +13541,10 @@ packages: '@babel/runtime': 7.24.0 dev: true + /hls.js@1.5.15: + resolution: {integrity: sha512-6cD7xN6bycBHaXz2WyPIaHn/iXFizE5au2yvY5q9aC4wfihxAr16C9fUy4nxh2a3wOw0fEgLRa9dN6wsYjlpNg==} + dev: false + /hogan.js@3.0.2: resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==} hasBin: true