Ios player v2 (#1901)
* feat(ui): new ios player start * fix(ui): image player * fix(ui): fix autoplay * fix(ui): fix copy paste code; error handler; default mode null * fix(ui): fix loader animation * fix(ui): memory cleanup * fix(ui): memory cleanup
This commit is contained in:
parent
3cea5d7d0b
commit
e207d37e69
13 changed files with 459 additions and 245 deletions
|
|
@ -1,62 +1,168 @@
|
|||
import React from 'react'
|
||||
import { PlayerMode } from 'Player';
|
||||
import React from 'react';
|
||||
import { MobilePlayerContext, IOSPlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { mapIphoneModel } from "Player/mobile/utils";
|
||||
import { mapIphoneModel } from 'Player/mobile/utils';
|
||||
|
||||
interface Props {
|
||||
videoURL: string;
|
||||
videoURL: string[];
|
||||
userDevice: string;
|
||||
}
|
||||
|
||||
const appleIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" fill="white" viewBox="0 0 16 16">
|
||||
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.386 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z"/>
|
||||
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.386 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z"/>
|
||||
</svg>`
|
||||
</svg>`;
|
||||
|
||||
function ReplayWindow({ videoURL, userDevice }: Props) {
|
||||
const playerContext = React.useContext<IOSPlayerContext>(MobilePlayerContext);
|
||||
const videoRef = React.useRef<HTMLVideoElement>();
|
||||
const imageRef = React.useRef<HTMLImageElement>();
|
||||
const containerRef = React.useRef<HTMLDivElement>();
|
||||
|
||||
const time = playerContext.store.get().time
|
||||
const { time, currentSnapshot, mode } = playerContext.store.get();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (videoRef.current) {
|
||||
const timeSecs = time / 1000
|
||||
const delta = videoRef.current.currentTime - timeSecs
|
||||
if (videoRef.current && mode === PlayerMode.VIDEO) {
|
||||
const timeSecs = time / 1000;
|
||||
const delta = videoRef.current.currentTime - timeSecs;
|
||||
if (videoRef.current.duration >= timeSecs && Math.abs(delta) > 0.1) {
|
||||
videoRef.current.currentTime = timeSecs
|
||||
videoRef.current.currentTime = timeSecs;
|
||||
}
|
||||
}
|
||||
}, [time])
|
||||
}, [time, mode]);
|
||||
React.useEffect(() => {
|
||||
if (currentSnapshot && mode === PlayerMode.SNAPS) {
|
||||
const blob = currentSnapshot.getBlobUrl();
|
||||
if (imageRef.current) {
|
||||
imageRef.current.src = blob;
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
if (imageRef.current) {
|
||||
URL.revokeObjectURL(imageRef.current.src)
|
||||
}
|
||||
}
|
||||
}, [currentSnapshot, mode]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (playerContext.player.screen.document && videoURL) {
|
||||
playerContext.player.pause()
|
||||
const { svg, styles } = mapIphoneModel(userDevice)
|
||||
const { svg, styles } = mapIphoneModel(userDevice);
|
||||
if (!containerRef.current && playerContext.player.screen.document) {
|
||||
const host = document.createElement('div');
|
||||
const shell = document.createElement('div');
|
||||
const icon = document.createElement('div');
|
||||
const videoContainer = document.createElement('div');
|
||||
|
||||
const host = document.createElement('div')
|
||||
const videoEl = document.createElement('video')
|
||||
const sourceEl = document.createElement('source')
|
||||
const shell = document.createElement('div')
|
||||
const icon = document.createElement('div')
|
||||
const videoContainer = document.createElement('div')
|
||||
videoContainer.style.borderRadius = '10px';
|
||||
videoContainer.style.overflow = 'hidden';
|
||||
videoContainer.style.margin = styles.margin;
|
||||
videoContainer.style.display = 'none';
|
||||
videoContainer.style.width = styles.screen.width + 'px';
|
||||
videoContainer.style.height = styles.screen.height + 'px';
|
||||
|
||||
videoContainer.style.borderRadius = '10px'
|
||||
videoContainer.style.overflow = 'hidden'
|
||||
videoContainer.style.margin = styles.margin
|
||||
videoContainer.style.display = 'none'
|
||||
videoContainer.style.width = styles.screen.width + 'px'
|
||||
videoContainer.style.height = styles.screen.height + 'px'
|
||||
shell.innerHTML = svg;
|
||||
Object.assign(icon.style, mobileIconStyle(styles));
|
||||
const spacer = document.createElement('div');
|
||||
spacer.style.width = '60px';
|
||||
spacer.style.height = '60px';
|
||||
|
||||
videoContainer.appendChild(videoEl)
|
||||
const loadingBar = document.createElement('div');
|
||||
|
||||
shell.innerHTML = svg
|
||||
Object.assign(loadingBar.style, mobileLoadingBarStyle(styles));
|
||||
icon.innerHTML = appleIcon;
|
||||
|
||||
videoEl.width = styles.screen.width
|
||||
videoEl.height = styles.screen.height
|
||||
videoEl.style.backgroundColor = '#333'
|
||||
shell.style.position = 'absolute';
|
||||
shell.style.top = '0';
|
||||
|
||||
Object.assign(icon.style, {
|
||||
host.appendChild(videoContainer);
|
||||
host.appendChild(shell);
|
||||
|
||||
icon.appendChild(spacer);
|
||||
icon.appendChild(loadingBar);
|
||||
host.appendChild(icon);
|
||||
|
||||
containerRef.current = host;
|
||||
videoContainer.id = '___or_replay-video';
|
||||
icon.id = '___or_ios-icon';
|
||||
host.id = '___or_ios-player';
|
||||
|
||||
playerContext.player.injectPlayer(host);
|
||||
playerContext.player.customScale(styles.shell.width, styles.shell.height);
|
||||
playerContext.player.updateDimensions({
|
||||
width: styles.screen.width,
|
||||
height: styles.screen.height,
|
||||
});
|
||||
playerContext.player.updateOverlayStyle({
|
||||
margin: styles.margin,
|
||||
width: styles.screen.width + 'px',
|
||||
height: styles.screen.height + 'px',
|
||||
});
|
||||
}
|
||||
}, [playerContext.player.screen.document]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const { styles } = mapIphoneModel(userDevice);
|
||||
if (mode) {
|
||||
const host = containerRef.current;
|
||||
const videoContainer =
|
||||
playerContext.player.screen.document?.getElementById('___or_replay-video');
|
||||
const icon = playerContext.player.screen.document?.getElementById('___or_ios-icon');
|
||||
if (host && videoContainer && icon) {
|
||||
if (mode === PlayerMode.SNAPS) {
|
||||
const imagePlayer = document.createElement('img');
|
||||
imagePlayer.style.width = styles.screen.width + 'px';
|
||||
imagePlayer.style.height = styles.screen.height + 'px';
|
||||
imagePlayer.style.backgroundColor = '#333';
|
||||
|
||||
videoContainer.appendChild(imagePlayer);
|
||||
const removeLoader = () => {
|
||||
host.removeChild(icon);
|
||||
videoContainer.style.display = 'block';
|
||||
imagePlayer.removeEventListener('load', removeLoader);
|
||||
};
|
||||
imagePlayer.addEventListener('load', removeLoader);
|
||||
imageRef.current = imagePlayer;
|
||||
playerContext.player.play();
|
||||
}
|
||||
if (mode === PlayerMode.VIDEO) {
|
||||
const mp4URL = videoURL.find((url) => url.includes('.mp4'));
|
||||
if (mp4URL) {
|
||||
const videoEl = document.createElement('video');
|
||||
const sourceEl = document.createElement('source');
|
||||
|
||||
videoContainer.appendChild(videoEl);
|
||||
|
||||
videoEl.width = styles.screen.width;
|
||||
videoEl.height = styles.screen.height;
|
||||
videoEl.style.backgroundColor = '#333';
|
||||
|
||||
sourceEl.setAttribute('src', mp4URL);
|
||||
sourceEl.setAttribute('type', 'video/mp4');
|
||||
videoEl.appendChild(sourceEl);
|
||||
|
||||
videoEl.addEventListener('loadeddata', () => {
|
||||
host.removeChild(icon);
|
||||
videoContainer.style.display = 'block';
|
||||
playerContext.player.play();
|
||||
});
|
||||
|
||||
videoRef.current = videoEl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [videoURL, playerContext.player.screen.document, mode]);
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const mobileLoadingBarStyle = (styles: any) => ({
|
||||
width: styles.screen.width / 2 + 'px',
|
||||
height: '6px',
|
||||
borderRadius: '3px',
|
||||
backgroundColor: 'white',
|
||||
});
|
||||
const mobileIconStyle = (styles: any) => ({
|
||||
backgroundColor: '#333',
|
||||
borderRadius: '10px',
|
||||
width: styles.screen.width + 'px',
|
||||
|
|
@ -66,58 +172,6 @@ function ReplayWindow({ videoURL, userDevice }: Props) {
|
|||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
})
|
||||
const spacer = document.createElement('div')
|
||||
spacer.style.width = '60px'
|
||||
spacer.style.height = '60px'
|
||||
|
||||
const loadingBar = document.createElement('div')
|
||||
Object.assign(loadingBar.style, {
|
||||
width: styles.screen.width/2 + 'px',
|
||||
height: '6px',
|
||||
borderRadius: '3px',
|
||||
backgroundColor: 'white',
|
||||
})
|
||||
icon.innerHTML = appleIcon
|
||||
icon.appendChild(spacer)
|
||||
icon.appendChild(loadingBar)
|
||||
|
||||
shell.style.position = 'absolute'
|
||||
shell.style.top = '0'
|
||||
|
||||
sourceEl.setAttribute('src', videoURL)
|
||||
sourceEl.setAttribute('type', 'video/mp4')
|
||||
|
||||
host.appendChild(videoContainer)
|
||||
host.appendChild(shell)
|
||||
host.appendChild(icon)
|
||||
videoEl.appendChild(sourceEl)
|
||||
|
||||
videoEl.addEventListener("loadeddata", () => {
|
||||
videoContainer.style.display = 'block'
|
||||
icon.style.display = 'none'
|
||||
host.removeChild(icon)
|
||||
console.log('loaded')
|
||||
playerContext.player.play()
|
||||
})
|
||||
|
||||
videoRef.current = videoEl
|
||||
playerContext.player.injectPlayer(host)
|
||||
playerContext.player.customScale(styles.shell.width, styles.shell.height)
|
||||
playerContext.player.updateDimensions({
|
||||
width: styles.screen.width,
|
||||
height: styles.screen.height,
|
||||
})
|
||||
playerContext.player.updateOverlayStyle({
|
||||
margin: styles.margin,
|
||||
width: styles.screen.width + 'px',
|
||||
height: styles.screen.height + 'px',
|
||||
})
|
||||
}
|
||||
}, [videoURL, playerContext.player.screen.document])
|
||||
return (
|
||||
<div />
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
export default observer(ReplayWindow);
|
||||
11
frontend/app/player/common/common.d.ts
vendored
Normal file
11
frontend/app/player/common/common.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
declare module 'js-untar' {
|
||||
export interface TarFile {
|
||||
name: string
|
||||
blob: Blob
|
||||
buffer: ArrayBuffer
|
||||
getBlobUrl: () => string
|
||||
}
|
||||
|
||||
export default function untar(tarFile: ArrayBuffer): Promise<TarFile[]>
|
||||
}
|
||||
25
frontend/app/player/common/tarball.ts
Normal file
25
frontend/app/player/common/tarball.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import untar, { TarFile } from 'js-untar';
|
||||
|
||||
const unpackTar = (data: Uint8Array): Promise<TarFile[]> => {
|
||||
const isTar = true
|
||||
// tarball ustar starts from 257, 75 73 74 61 72, but this is getting lost here for some reason
|
||||
// so we rely on try catch
|
||||
// data[257] === 0x75 &&
|
||||
// data[258] === 0x73 &&
|
||||
// data[259] === 0x74 &&
|
||||
// data[260] === 0x61 &&
|
||||
// data[261] === 0x72 &&
|
||||
// data[262] === 0x00;
|
||||
|
||||
if (isTar) {
|
||||
const now = performance.now();
|
||||
return untar(data.buffer).then((files) => {
|
||||
console.debug('Tar unpack time', Math.floor(performance.now() - now) + 'ms');
|
||||
return files;
|
||||
});
|
||||
} else {
|
||||
return Promise.reject('Not a tarball file');
|
||||
}
|
||||
};
|
||||
|
||||
export default unpackTar;
|
||||
|
|
@ -39,6 +39,7 @@ export interface SessionFilesInfo {
|
|||
milliseconds: number
|
||||
valueOf: () => number
|
||||
}
|
||||
videoURL: string[]
|
||||
domURL: string[]
|
||||
devtoolsURL: string[]
|
||||
/** deprecated */
|
||||
|
|
|
|||
|
|
@ -1,39 +1,40 @@
|
|||
import * as fzstd from 'fzstd';
|
||||
import { gunzipSync } from 'fflate'
|
||||
import { gunzipSync } from 'fflate';
|
||||
|
||||
const unpack = (b: Uint8Array): Uint8Array => {
|
||||
// zstd magical numbers 40 181 47 253
|
||||
const isZstd = b[0] === 0x28 && b[1] === 0xb5 && b[2] === 0x2f && b[3] === 0xfd
|
||||
const isGzip = b[0] === 0x1F && b[1] === 0x8B && b[2] === 0x08;
|
||||
const isZstd = b[0] === 0x28 && b[1] === 0xb5 && b[2] === 0x2f && b[3] === 0xfd;
|
||||
const isGzip = b[0] === 0x1f && b[1] === 0x8b && b[2] === 0x08;
|
||||
let data = b;
|
||||
if (isGzip) {
|
||||
const now = performance.now()
|
||||
const data = gunzipSync(b)
|
||||
const now = performance.now();
|
||||
const uData = gunzipSync(b);
|
||||
console.debug(
|
||||
"Gunzip time",
|
||||
'Gunzip time',
|
||||
Math.floor(performance.now() - now) + 'ms',
|
||||
'size',
|
||||
Math.floor(b.byteLength / 1024),
|
||||
'->',
|
||||
Math.floor(data.byteLength / 1024),
|
||||
Math.floor(uData.byteLength / 1024),
|
||||
'kb'
|
||||
)
|
||||
return data
|
||||
);
|
||||
data = uData;
|
||||
}
|
||||
if (isZstd) {
|
||||
const now = performance.now()
|
||||
const data = fzstd.decompress(b)
|
||||
const now = performance.now();
|
||||
const uData = fzstd.decompress(b);
|
||||
console.debug(
|
||||
"Zstd unpack time",
|
||||
'Zstd unpack time',
|
||||
Math.floor(performance.now() - now) + 'ms',
|
||||
'size',
|
||||
Math.floor(b.byteLength / 1024),
|
||||
'->',
|
||||
Math.floor(data.byteLength / 1024),
|
||||
Math.floor(uData.byteLength / 1024),
|
||||
'kb'
|
||||
)
|
||||
return data
|
||||
);
|
||||
data = uData;
|
||||
}
|
||||
return b
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
export default unpack
|
||||
export default unpack;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import logger from 'App/logger';
|
||||
import { getResourceFromNetworkRequest } from "Player";
|
||||
import { TarFile } from "js-untar";
|
||||
import { getResourceFromNetworkRequest } from 'Player';
|
||||
|
||||
import type { Store } from 'Player';
|
||||
import { IMessageManager } from 'Player/player/Animator';
|
||||
|
|
@ -11,43 +12,52 @@ import Lists, {
|
|||
INITIAL_STATE as LISTS_INITIAL_STATE,
|
||||
State as ListsState,
|
||||
} from './IOSLists';
|
||||
import IOSPerformanceTrackManager, { PerformanceChartPoint } from "Player/mobile/managers/IOSPerformanceTrackManager";
|
||||
import IOSPerformanceTrackManager, {
|
||||
PerformanceChartPoint,
|
||||
} from 'Player/mobile/managers/IOSPerformanceTrackManager';
|
||||
import { MType } from '../web/messages';
|
||||
import type { Message } from '../web/messages';
|
||||
import SnapshotManager from 'Player/mobile/managers/SnapshotManager';
|
||||
|
||||
import Screen, {
|
||||
INITIAL_STATE as SCREEN_INITIAL_STATE,
|
||||
State as ScreenState,
|
||||
} from '../web/Screen/Screen';
|
||||
|
||||
import { Log } from './types/log'
|
||||
import { Log } from './types/log';
|
||||
|
||||
import type { SkipInterval } from '../web/managers/ActivityManager';
|
||||
|
||||
export const performanceWarnings = ['thermalState', 'memoryWarning', 'lowDiskSpace', 'isLowPowerModeEnabled', 'batteryLevel']
|
||||
export const performanceWarnings = [
|
||||
'thermalState',
|
||||
'memoryWarning',
|
||||
'lowDiskSpace',
|
||||
'isLowPowerModeEnabled',
|
||||
'batteryLevel',
|
||||
];
|
||||
|
||||
const perfWarningFrustrations = {
|
||||
thermalState: {
|
||||
title: "Overheating",
|
||||
icon: "thermometer-sun",
|
||||
title: 'Overheating',
|
||||
icon: 'thermometer-sun',
|
||||
},
|
||||
memoryWarning: {
|
||||
title: "High Memory Usage",
|
||||
icon: "memory-ios"
|
||||
title: 'High Memory Usage',
|
||||
icon: 'memory-ios',
|
||||
},
|
||||
lowDiskSpace: {
|
||||
title: "Low Disk Space",
|
||||
icon: "low-disc-space"
|
||||
title: 'Low Disk Space',
|
||||
icon: 'low-disc-space',
|
||||
},
|
||||
isLowPowerModeEnabled: {
|
||||
title: "Low Power Mode",
|
||||
icon: "battery-charging"
|
||||
title: 'Low Power Mode',
|
||||
icon: 'battery-charging',
|
||||
},
|
||||
batteryLevel: {
|
||||
title: "Low Battery",
|
||||
icon: "battery"
|
||||
}
|
||||
}
|
||||
title: 'Low Battery',
|
||||
icon: 'battery',
|
||||
},
|
||||
};
|
||||
|
||||
export interface State extends ScreenState, ListsState {
|
||||
skipIntervals: SkipInterval[];
|
||||
|
|
@ -64,9 +74,15 @@ export interface State extends ScreenState, ListsState {
|
|||
messagesProcessed: boolean;
|
||||
eventCount: number;
|
||||
updateWarnings: number;
|
||||
currentSnapshot: TarFile | null;
|
||||
}
|
||||
|
||||
const userEvents = [MType.IosSwipeEvent, MType.IosClickEvent, MType.IosInputEvent, MType.IosScreenChanges];
|
||||
const userEvents = [
|
||||
MType.IosSwipeEvent,
|
||||
MType.IosClickEvent,
|
||||
MType.IosInputEvent,
|
||||
MType.IosScreenChanges,
|
||||
];
|
||||
|
||||
export default class IOSMessageManager implements IMessageManager {
|
||||
static INITIAL_STATE: State = {
|
||||
|
|
@ -83,6 +99,7 @@ export default class IOSMessageManager implements IMessageManager {
|
|||
lastMessageTime: 0,
|
||||
messagesProcessed: false,
|
||||
messagesLoading: false,
|
||||
currentSnapshot: null,
|
||||
};
|
||||
|
||||
private activityManager: ActivityManager | null = null;
|
||||
|
|
@ -92,6 +109,7 @@ export default class IOSMessageManager implements IMessageManager {
|
|||
private lastMessageTime: number = 0;
|
||||
private touchManager: TouchManager;
|
||||
private lists: Lists;
|
||||
public snapshotManager: SnapshotManager;
|
||||
|
||||
constructor(
|
||||
private readonly session: Record<string, any>,
|
||||
|
|
@ -104,6 +122,7 @@ export default class IOSMessageManager implements IMessageManager {
|
|||
this.lists = new Lists(initialLists);
|
||||
this.touchManager = new TouchManager(screen);
|
||||
this.activityManager = new ActivityManager(this.session.duration.milliseconds); // only if not-live
|
||||
this.snapshotManager = new SnapshotManager();
|
||||
}
|
||||
|
||||
public updateDimensions(dimensions: { width: number; height: number }) {
|
||||
|
|
@ -111,16 +130,16 @@ export default class IOSMessageManager implements IMessageManager {
|
|||
}
|
||||
|
||||
public updateLists(lists: Partial<InitialLists>) {
|
||||
const exceptions = lists.exceptions
|
||||
exceptions?.forEach(e => {
|
||||
const exceptions = lists.exceptions;
|
||||
exceptions?.forEach((e) => {
|
||||
this.lists.lists.exceptions.insert(e);
|
||||
this.lists.lists.log.insert(e)
|
||||
})
|
||||
lists.frustrations?.forEach(f => {
|
||||
this.lists.lists.log.insert(e);
|
||||
});
|
||||
lists.frustrations?.forEach((f) => {
|
||||
this.lists.lists.frustrations.insert(f);
|
||||
})
|
||||
});
|
||||
|
||||
const eventCount = this.lists.lists.event.count //lists?.event?.length || 0;
|
||||
const eventCount = this.lists.lists.event.count; //lists?.event?.length || 0;
|
||||
const currentState = this.state.get();
|
||||
this.state.update({
|
||||
eventCount: currentState.eventCount + eventCount,
|
||||
|
|
@ -135,7 +154,7 @@ export default class IOSMessageManager implements IMessageManager {
|
|||
|
||||
public getListsFullState = () => {
|
||||
return this.lists.getFullListsState();
|
||||
}
|
||||
};
|
||||
|
||||
private waitingForFiles: boolean = false;
|
||||
public onFileReadSuccess = () => {
|
||||
|
|
@ -144,29 +163,29 @@ export default class IOSMessageManager implements IMessageManager {
|
|||
eventCount: this.lists?.lists.event?.length || 0,
|
||||
performanceChartData: this.performanceManager.chartData,
|
||||
...this.lists.getFullListsState(),
|
||||
}
|
||||
};
|
||||
|
||||
if (this.activityManager) {
|
||||
this.activityManager.end();
|
||||
newState['skipIntervals'] = this.activityManager.list
|
||||
newState['skipIntervals'] = this.activityManager.list;
|
||||
}
|
||||
this.state.update(newState);
|
||||
};
|
||||
|
||||
public onFileReadFailed = (...e: any[]) => {
|
||||
logger.error(e);
|
||||
this.state.update({error: true});
|
||||
this.state.update({ error: true });
|
||||
this.uiErrorHandler?.error('Error requesting a session file');
|
||||
};
|
||||
|
||||
public onFileReadFinally = () => {
|
||||
this.waitingForFiles = false;
|
||||
this.state.update({messagesProcessed: true});
|
||||
this.state.update({ messagesProcessed: true });
|
||||
};
|
||||
|
||||
public startLoading = () => {
|
||||
this.waitingForFiles = true;
|
||||
this.state.update({messagesProcessed: false});
|
||||
this.state.update({ messagesProcessed: false });
|
||||
this.setMessagesLoading(true);
|
||||
};
|
||||
|
||||
|
|
@ -182,7 +201,7 @@ export default class IOSMessageManager implements IMessageManager {
|
|||
if (lastPerformanceTrackMessage) {
|
||||
Object.assign(stateToUpdate, {
|
||||
performanceChartTime: lastPerformanceTrackMessage.time,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
this.touchManager.move(t);
|
||||
|
|
@ -194,61 +213,67 @@ export default class IOSMessageManager implements IMessageManager {
|
|||
this.setMessagesLoading(true);
|
||||
}
|
||||
|
||||
Object.assign(stateToUpdate, this.lists.moveGetState(t))
|
||||
Object.assign(stateToUpdate, { performanceListNow: this.lists.lists.performance.listNow })
|
||||
const snapshot = this.snapshotManager.moveReady(t);
|
||||
if (snapshot) {
|
||||
Object.assign(stateToUpdate, {
|
||||
currentSnapshot: snapshot,
|
||||
});
|
||||
}
|
||||
Object.assign(stateToUpdate, this.lists.moveGetState(t));
|
||||
Object.assign(stateToUpdate, { performanceListNow: this.lists.lists.performance.listNow });
|
||||
Object.keys(stateToUpdate).length > 0 && this.state.update(stateToUpdate);
|
||||
}
|
||||
|
||||
distributeMessage = (msg: Message & { tabId: string }): void => {
|
||||
const lastMessageTime = Math.max(msg.time, this.lastMessageTime);
|
||||
this.lastMessageTime = lastMessageTime;
|
||||
this.state.update({lastMessageTime});
|
||||
this.state.update({ lastMessageTime });
|
||||
if (userEvents.includes(msg.tp)) {
|
||||
this.activityManager?.updateAcctivity(msg.time);
|
||||
}
|
||||
|
||||
switch (msg.tp) {
|
||||
case MType.IosPerformanceEvent:
|
||||
const performanceStats = ['background', 'memoryUsage', 'mainThreadCPU']
|
||||
const performanceStats = ['background', 'memoryUsage', 'mainThreadCPU'];
|
||||
if (performanceStats.includes(msg.name)) {
|
||||
this.performanceManager.append(msg);
|
||||
}
|
||||
if (performanceWarnings.includes(msg.name)) {
|
||||
// @ts-ignore
|
||||
const item = perfWarningFrustrations[msg.name]
|
||||
const item = perfWarningFrustrations[msg.name];
|
||||
this.lists.lists.performance.append({
|
||||
...msg,
|
||||
name: item.title,
|
||||
techName: msg.name,
|
||||
icon: item.icon,
|
||||
type: 'ios_perf_event'
|
||||
} as any)
|
||||
type: 'ios_perf_event',
|
||||
} as any);
|
||||
}
|
||||
break;
|
||||
// case MType.IosInputEvent:
|
||||
// console.log('input', msg)
|
||||
// break;
|
||||
case MType.IosNetworkCall:
|
||||
this.lists.lists.fetch.insert(getResourceFromNetworkRequest(msg, this.sessionStart))
|
||||
this.lists.lists.fetch.insert(getResourceFromNetworkRequest(msg, this.sessionStart));
|
||||
break;
|
||||
case MType.WsChannel:
|
||||
this.lists.lists.websocket.insert(msg)
|
||||
this.lists.lists.websocket.insert(msg);
|
||||
break;
|
||||
case MType.IosEvent:
|
||||
// @ts-ignore
|
||||
this.lists.lists.event.insert({...msg, source: 'openreplay'});
|
||||
this.lists.lists.event.insert({ ...msg, source: 'openreplay' });
|
||||
break;
|
||||
case MType.IosSwipeEvent:
|
||||
case MType.IosClickEvent:
|
||||
this.touchManager.append(msg);
|
||||
break;
|
||||
case MType.IosLog:
|
||||
const log = {...msg, level: msg.severity}
|
||||
const log = { ...msg, level: msg.severity };
|
||||
// @ts-ignore
|
||||
this.lists.lists.log.append(Log(log));
|
||||
break;
|
||||
default:
|
||||
console.log(msg)
|
||||
console.log(msg);
|
||||
// stuff
|
||||
break;
|
||||
}
|
||||
|
|
@ -257,16 +282,17 @@ export default class IOSMessageManager implements IMessageManager {
|
|||
setMessagesLoading = (messagesLoading: boolean) => {
|
||||
this.screen.display(!messagesLoading);
|
||||
// @ts-ignore idk
|
||||
this.state.update({messagesLoading, ready: !messagesLoading && !this.state.get().cssLoading});
|
||||
this.state.update({ messagesLoading, ready: !messagesLoading && !this.state.get().cssLoading });
|
||||
};
|
||||
|
||||
private setSize({height, width}: { height: number; width: number }) {
|
||||
this.screen.scale({height, width});
|
||||
this.state.update({width, height});
|
||||
private setSize({ height, width }: { height: number; width: number }) {
|
||||
this.screen.scale({ height, width });
|
||||
this.state.update({ width, height });
|
||||
}
|
||||
|
||||
// TODO: clean managers?
|
||||
clean() {
|
||||
this.snapshotManager?.clean();
|
||||
this.state.update(IOSMessageManager.INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import { Log, LogLevel, SessionFilesInfo } from 'Player'
|
||||
import { Log, LogLevel, SessionFilesInfo } from 'Player';
|
||||
|
||||
import type { Store } from 'Player'
|
||||
import MessageLoader from "Player/web/MessageLoader";
|
||||
import Player from '../player/Player'
|
||||
import Screen, { ScaleMode } from '../web/Screen/Screen'
|
||||
import IOSMessageManager from "Player/mobile/IOSMessageManager";
|
||||
import type { Store } from 'Player';
|
||||
import MessageLoader from 'Player/web/MessageLoader';
|
||||
import Player from '../player/Player';
|
||||
import Screen, { ScaleMode } from '../web/Screen/Screen';
|
||||
import IOSMessageManager from 'Player/mobile/IOSMessageManager';
|
||||
|
||||
export const PlayerMode = {
|
||||
VIDEO: 'video',
|
||||
SNAPS: 'snaps',
|
||||
};
|
||||
|
||||
export default class IOSPlayer extends Player {
|
||||
static readonly INITIAL_STATE = {
|
||||
|
|
@ -12,56 +17,76 @@ export default class IOSPlayer extends Player {
|
|||
...MessageLoader.INITIAL_STATE,
|
||||
...IOSMessageManager.INITIAL_STATE,
|
||||
scale: 1,
|
||||
}
|
||||
public screen: Screen
|
||||
protected messageManager: IOSMessageManager
|
||||
protected readonly messageLoader: MessageLoader
|
||||
mode: null,
|
||||
autoplay: false,
|
||||
};
|
||||
public screen: Screen;
|
||||
protected messageManager: IOSMessageManager;
|
||||
protected readonly messageLoader: MessageLoader;
|
||||
|
||||
constructor(
|
||||
protected wpState: Store<any>,
|
||||
session: SessionFilesInfo,
|
||||
public readonly uiErrorHandler?: { error: (msg: string) => void }
|
||||
) {
|
||||
const screen = new Screen(true, ScaleMode.Embed)
|
||||
const messageManager = new IOSMessageManager(session, wpState, screen, uiErrorHandler)
|
||||
const hasTar = session.videoURL.some((url) => url.includes('.tar.'));
|
||||
const screen = new Screen(true, ScaleMode.Embed);
|
||||
const messageManager = new IOSMessageManager(session, wpState, screen, uiErrorHandler);
|
||||
const messageLoader = new MessageLoader(
|
||||
session,
|
||||
wpState,
|
||||
messageManager,
|
||||
false,
|
||||
uiErrorHandler
|
||||
)
|
||||
);
|
||||
super(wpState, messageManager);
|
||||
this.screen = screen
|
||||
this.messageManager = messageManager
|
||||
this.messageLoader = messageLoader
|
||||
this.pause()
|
||||
this.screen = screen;
|
||||
this.messageManager = messageManager;
|
||||
this.messageLoader = messageLoader;
|
||||
|
||||
void messageLoader.loadFiles()
|
||||
const endTime = session.duration?.valueOf() || 0
|
||||
if (hasTar) {
|
||||
messageLoader
|
||||
.loadTarball(session.videoURL.find((url) => url.includes('.tar.'))!)
|
||||
.then((files) => {
|
||||
if (files) {
|
||||
this.wpState.update({ mode: PlayerMode.SNAPS });
|
||||
this.messageManager.snapshotManager.mapToSnapshots(files);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
this.wpState.update({ mode: PlayerMode.VIDEO });
|
||||
});
|
||||
}
|
||||
void messageLoader.loadFiles();
|
||||
const endTime = session.duration?.valueOf() || 0;
|
||||
|
||||
wpState.update({
|
||||
session,
|
||||
endTime,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
attach = (parent: HTMLElement) => {
|
||||
this.screen.attach(parent)
|
||||
}
|
||||
this.screen.attach(parent);
|
||||
};
|
||||
|
||||
public updateDimensions(dimensions: { width: number; height: number }) {
|
||||
return this.messageManager.updateDimensions(dimensions)
|
||||
return this.messageManager.updateDimensions(dimensions);
|
||||
}
|
||||
|
||||
public updateLists(session: any) {
|
||||
const exceptions = session.crashes.concat(session.errors || [])
|
||||
const exceptions = session.crashes.concat(session.errors || []);
|
||||
const lists = {
|
||||
event: session.events.map((e: Record<string, any>) => {
|
||||
if (e.name === 'Click') e.name = 'Touch'
|
||||
return e
|
||||
event:
|
||||
session.events.map((e: Record<string, any>) => {
|
||||
if (e.name === 'Click') e.name = 'Touch';
|
||||
return e;
|
||||
}) || [],
|
||||
frustrations: session.frustrations || [],
|
||||
stack: session.stackEvents || [],
|
||||
exceptions: exceptions.map(({ name, ...rest }: any) =>
|
||||
exceptions:
|
||||
exceptions.map(({ name, ...rest }: any) =>
|
||||
Log({
|
||||
level: LogLevel.ERROR,
|
||||
value: name,
|
||||
|
|
@ -71,61 +96,58 @@ export default class IOSPlayer extends Player {
|
|||
...rest,
|
||||
})
|
||||
) || [],
|
||||
}
|
||||
};
|
||||
|
||||
return this.messageManager.updateLists(lists)
|
||||
return this.messageManager.updateLists(lists);
|
||||
}
|
||||
|
||||
public updateOverlayStyle(style: Partial<CSSStyleDeclaration>) {
|
||||
this.screen.updateOverlayStyle(style)
|
||||
this.screen.updateOverlayStyle(style);
|
||||
}
|
||||
|
||||
injectPlayer = (player: HTMLElement) => {
|
||||
this.screen.addToBody(player)
|
||||
this.screen.addMobileStyles()
|
||||
this.screen.addToBody(player);
|
||||
this.screen.addMobileStyles();
|
||||
window.addEventListener('resize', () =>
|
||||
this.customScale(this.customConstrains.width, this.customConstrains.height)
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
scale = () => {
|
||||
// const { width, height } = this.wpState.get()
|
||||
if (!this.screen) return;
|
||||
console.debug("using customConstrains to scale player")
|
||||
console.debug('using customConstrains to scale player');
|
||||
// sometimes happens in live assist sessions for some reason
|
||||
this.screen?.scale?.(this.customConstrains)
|
||||
}
|
||||
this.screen?.scale?.(this.customConstrains);
|
||||
};
|
||||
|
||||
customConstrains = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
};
|
||||
customScale = (width: number, height: number) => {
|
||||
if (!this.screen) return;
|
||||
this.screen?.scale?.({ width, height })
|
||||
this.customConstrains = { width, height }
|
||||
this.wpState.update({ scale: this.screen.getScale() })
|
||||
}
|
||||
this.screen?.scale?.({ width, height });
|
||||
this.customConstrains = { width, height };
|
||||
this.wpState.update({ scale: this.screen.getScale() });
|
||||
};
|
||||
|
||||
addFullscreenBoundary = (isFullscreen?: boolean) => {
|
||||
if (isFullscreen) {
|
||||
this.screen?.addFullscreenBoundary()
|
||||
this.screen?.addFullscreenBoundary();
|
||||
} else {
|
||||
this.screen?.addMobileStyles()
|
||||
this.screen?.addMobileStyles();
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
clean = () => {
|
||||
super.clean()
|
||||
this.screen.clean()
|
||||
super.clean();
|
||||
this.screen.clean();
|
||||
// @ts-ignore
|
||||
this.screen = undefined;
|
||||
this.messageLoader.clean()
|
||||
this.messageLoader.clean();
|
||||
// @ts-ignore
|
||||
this.messageManager = undefined;
|
||||
window.removeEventListener('resize', this.scale)
|
||||
}
|
||||
|
||||
|
||||
window.removeEventListener('resize', this.scale);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
40
frontend/app/player/mobile/managers/SnapshotManager.ts
Normal file
40
frontend/app/player/mobile/managers/SnapshotManager.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { TarFile } from "js-untar";
|
||||
import ListWalker from 'Player/common/ListWalker';
|
||||
|
||||
interface Snapshots {
|
||||
[timestamp: number]: TarFile
|
||||
}
|
||||
|
||||
type Timestamp = { time: number }
|
||||
|
||||
|
||||
export default class SnapshotManager extends ListWalker<Timestamp> {
|
||||
private snapshots: Snapshots = {}
|
||||
|
||||
public mapToSnapshots(files: TarFile[]) {
|
||||
const filenameRegexp = /(\d+)_1_(\d+)\.jpeg$/;
|
||||
const firstPair = files[0].name.match(filenameRegexp)
|
||||
const sessionStart = firstPair ? parseInt(firstPair[1], 10) : 0
|
||||
files.forEach(file => {
|
||||
const [_, _2, imageTimestamp] = file
|
||||
.name
|
||||
.match(filenameRegexp)
|
||||
?.map(n => parseInt(n, 10)) ?? [0, 0, 0]
|
||||
const messageTime = imageTimestamp - sessionStart
|
||||
this.snapshots[messageTime] = file
|
||||
this.append({ time: messageTime })
|
||||
})
|
||||
}
|
||||
|
||||
public moveReady(t: number) {
|
||||
const msg = this.moveGetLast(t)
|
||||
if (msg) {
|
||||
return this.snapshots[msg.time]
|
||||
}
|
||||
}
|
||||
|
||||
public clean() {
|
||||
this.snapshots = {}
|
||||
this.reset()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
import type { Store, SessionFilesInfo, PlayerMsg } from "Player";
|
||||
import type { Store, SessionFilesInfo, PlayerMsg } from 'Player';
|
||||
import { decryptSessionBytes } from './network/crypto';
|
||||
import MFileReader from './messages/MFileReader';
|
||||
import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles';
|
||||
import { loadFiles, requestEFSDom, requestEFSDevtools, requestTarball } from './network/loadFiles';
|
||||
import logger from 'App/logger';
|
||||
import unpack from 'Player/common/unpack';
|
||||
import MessageManager from 'Player/web/MessageManager';
|
||||
import IOSMessageManager from 'Player/mobile/IOSMessageManager';
|
||||
import unpackTar from 'Player/common/tarball';
|
||||
|
||||
interface State {
|
||||
firstFileLoading: boolean;
|
||||
|
|
@ -79,6 +80,18 @@ export default class MessageLoader {
|
|||
this.messageManager.setMessagesLoading(false);
|
||||
};
|
||||
|
||||
async loadTarball(url: string) {
|
||||
try {
|
||||
const tarBufferZstd = await requestTarball(url);
|
||||
if (tarBufferZstd) {
|
||||
const tar = unpack(tarBufferZstd);
|
||||
return await unpackTar(tar);
|
||||
}
|
||||
} catch (e) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async loadDomFiles(urls: string[], parser: (b: Uint8Array) => Promise<void>) {
|
||||
if (urls.length > 0) {
|
||||
this.store.update({ domLoading: true });
|
||||
|
|
@ -116,10 +129,10 @@ export default class MessageLoader {
|
|||
this.messageManager.startLoading();
|
||||
|
||||
try {
|
||||
await this.loadMobs()
|
||||
await this.loadMobs();
|
||||
} catch (sessionLoadError) {
|
||||
try {
|
||||
await this.loadEFSMobs()
|
||||
await this.loadEFSMobs();
|
||||
} catch (unprocessedLoadError) {
|
||||
this.messageManager.onFileReadFailed(sessionLoadError, unprocessedLoadError);
|
||||
}
|
||||
|
|
@ -157,7 +170,7 @@ export default class MessageLoader {
|
|||
|
||||
await Promise.allSettled([restDomFilesPromise, restDevtoolsFilesPromise]);
|
||||
this.messageManager.onFileReadSuccess();
|
||||
}
|
||||
};
|
||||
|
||||
loadEFSMobs = async () => {
|
||||
this.store.update({ domLoading: true, devtoolsLoading: true });
|
||||
|
|
@ -181,7 +194,7 @@ export default class MessageLoader {
|
|||
|
||||
await Promise.all([parseDomPromise, parseDevtoolsPromise]);
|
||||
this.messageManager.onFileReadSuccess();
|
||||
}
|
||||
};
|
||||
|
||||
clean() {
|
||||
this.store.update(MessageLoader.INITIAL_STATE);
|
||||
|
|
|
|||
|
|
@ -114,6 +114,8 @@ export default class Screen {
|
|||
if (this.document) {
|
||||
this.document.body.style.margin = '0';
|
||||
this.document.body.appendChild(el);
|
||||
} else {
|
||||
console.error('Attempt to add to player screen without document');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,16 @@ export async function requestEFSDevtools(sessionId: string) {
|
|||
return await requestEFSMobFile(sessionId + "/devtools.mob")
|
||||
}
|
||||
|
||||
export async function requestTarball(url: string) {
|
||||
const res = await window.fetch(url)
|
||||
if (res.ok) {
|
||||
const buf = await res.arrayBuffer()
|
||||
return new Uint8Array(buf)
|
||||
} else {
|
||||
throw new Error(res.status.toString())
|
||||
}
|
||||
}
|
||||
|
||||
async function requestEFSMobFile(filename: string) {
|
||||
const api = new APIClient()
|
||||
const res = await api.fetch('/unprocessed/' + filename)
|
||||
|
|
@ -58,13 +68,13 @@ async function requestEFSMobFile(filename: string) {
|
|||
}
|
||||
|
||||
const processAPIStreamResponse = (response: Response, skippable: boolean) => {
|
||||
return new Promise<Blob>((res, rej) => {
|
||||
return new Promise<ArrayBuffer>((res, rej) => {
|
||||
if (response.status === 404 && skippable) {
|
||||
return rej(ALLOWED_404)
|
||||
}
|
||||
if (response.status >= 400) {
|
||||
return rej(`Bad file status code ${response.status}. Url: ${response.url}`)
|
||||
}
|
||||
res(response.blob())
|
||||
}).then(async blob => new Uint8Array(await blob.arrayBuffer()))
|
||||
res(response.arrayBuffer())
|
||||
}).then(async buf => new Uint8Array(buf))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
"html2canvas": "^1.4.1",
|
||||
"immutable": "^4.0.0-rc.12",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"js-untar": "^2.0.0",
|
||||
"jsbi": "^4.1.0",
|
||||
"jshint": "^2.11.1",
|
||||
"jspdf": "^2.5.1",
|
||||
|
|
|
|||
|
|
@ -15261,6 +15261,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-untar@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "js-untar@npm:2.0.0"
|
||||
checksum: d84138561b2ed12870ba07ff62c55dfb0f9884cd1eb32044ba6272eacbed4ec476e08cc62226f5824d8bda3de3597a506582ff9bdf0845f221d3e092f9d8b025
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-yaml@npm:^3.13.1":
|
||||
version: 3.14.1
|
||||
resolution: "js-yaml@npm:3.14.1"
|
||||
|
|
@ -18152,6 +18159,7 @@ __metadata:
|
|||
immutable: ^4.0.0-rc.12
|
||||
jest: ^29.5.0
|
||||
jest-environment-jsdom: ^29.5.0
|
||||
js-untar: ^2.0.0
|
||||
jsbi: ^4.1.0
|
||||
jshint: ^2.11.1
|
||||
jspdf: ^2.5.1
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue