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,123 +1,177 @@
|
||||||
import React from 'react'
|
import { PlayerMode } from 'Player';
|
||||||
|
import React from 'react';
|
||||||
import { MobilePlayerContext, IOSPlayerContext } from 'App/components/Session/playerContext';
|
import { MobilePlayerContext, IOSPlayerContext } from 'App/components/Session/playerContext';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { mapIphoneModel } from "Player/mobile/utils";
|
import { mapIphoneModel } from 'Player/mobile/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
videoURL: string;
|
videoURL: string[];
|
||||||
userDevice: string;
|
userDevice: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appleIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" fill="white" viewBox="0 0 16 16">
|
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"/>
|
||||||
<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) {
|
function ReplayWindow({ videoURL, userDevice }: Props) {
|
||||||
const playerContext = React.useContext<IOSPlayerContext>(MobilePlayerContext);
|
const playerContext = React.useContext<IOSPlayerContext>(MobilePlayerContext);
|
||||||
const videoRef = React.useRef<HTMLVideoElement>();
|
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(() => {
|
React.useEffect(() => {
|
||||||
if (videoRef.current) {
|
if (videoRef.current && mode === PlayerMode.VIDEO) {
|
||||||
const timeSecs = time / 1000
|
const timeSecs = time / 1000;
|
||||||
const delta = videoRef.current.currentTime - timeSecs
|
const delta = videoRef.current.currentTime - timeSecs;
|
||||||
if (videoRef.current.duration >= timeSecs && Math.abs(delta) > 0.1) {
|
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(() => {
|
React.useEffect(() => {
|
||||||
if (playerContext.player.screen.document && videoURL) {
|
playerContext.player.pause()
|
||||||
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')
|
videoContainer.style.borderRadius = '10px';
|
||||||
const videoEl = document.createElement('video')
|
videoContainer.style.overflow = 'hidden';
|
||||||
const sourceEl = document.createElement('source')
|
videoContainer.style.margin = styles.margin;
|
||||||
const shell = document.createElement('div')
|
videoContainer.style.display = 'none';
|
||||||
const icon = document.createElement('div')
|
videoContainer.style.width = styles.screen.width + 'px';
|
||||||
const videoContainer = document.createElement('div')
|
videoContainer.style.height = styles.screen.height + 'px';
|
||||||
|
|
||||||
videoContainer.style.borderRadius = '10px'
|
shell.innerHTML = svg;
|
||||||
videoContainer.style.overflow = 'hidden'
|
Object.assign(icon.style, mobileIconStyle(styles));
|
||||||
videoContainer.style.margin = styles.margin
|
const spacer = document.createElement('div');
|
||||||
videoContainer.style.display = 'none'
|
spacer.style.width = '60px';
|
||||||
videoContainer.style.width = styles.screen.width + 'px'
|
spacer.style.height = '60px';
|
||||||
videoContainer.style.height = styles.screen.height + 'px'
|
|
||||||
|
|
||||||
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
|
shell.style.position = 'absolute';
|
||||||
videoEl.height = styles.screen.height
|
shell.style.top = '0';
|
||||||
videoEl.style.backgroundColor = '#333'
|
|
||||||
|
|
||||||
Object.assign(icon.style, {
|
host.appendChild(videoContainer);
|
||||||
backgroundColor: '#333',
|
host.appendChild(shell);
|
||||||
borderRadius: '10px',
|
|
||||||
width: styles.screen.width + 'px',
|
|
||||||
height: styles.screen.height + 'px',
|
|
||||||
margin: styles.margin,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
})
|
|
||||||
const spacer = document.createElement('div')
|
|
||||||
spacer.style.width = '60px'
|
|
||||||
spacer.style.height = '60px'
|
|
||||||
|
|
||||||
const loadingBar = document.createElement('div')
|
icon.appendChild(spacer);
|
||||||
Object.assign(loadingBar.style, {
|
icon.appendChild(loadingBar);
|
||||||
width: styles.screen.width/2 + 'px',
|
host.appendChild(icon);
|
||||||
height: '6px',
|
|
||||||
borderRadius: '3px',
|
|
||||||
backgroundColor: 'white',
|
|
||||||
})
|
|
||||||
icon.innerHTML = appleIcon
|
|
||||||
icon.appendChild(spacer)
|
|
||||||
icon.appendChild(loadingBar)
|
|
||||||
|
|
||||||
shell.style.position = 'absolute'
|
containerRef.current = host;
|
||||||
shell.style.top = '0'
|
videoContainer.id = '___or_replay-video';
|
||||||
|
icon.id = '___or_ios-icon';
|
||||||
|
host.id = '___or_ios-player';
|
||||||
|
|
||||||
sourceEl.setAttribute('src', videoURL)
|
playerContext.player.injectPlayer(host);
|
||||||
sourceEl.setAttribute('type', 'video/mp4')
|
playerContext.player.customScale(styles.shell.width, styles.shell.height);
|
||||||
|
|
||||||
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({
|
playerContext.player.updateDimensions({
|
||||||
width: styles.screen.width,
|
width: styles.screen.width,
|
||||||
height: styles.screen.height,
|
height: styles.screen.height,
|
||||||
})
|
});
|
||||||
playerContext.player.updateOverlayStyle({
|
playerContext.player.updateOverlayStyle({
|
||||||
margin: styles.margin,
|
margin: styles.margin,
|
||||||
width: styles.screen.width + 'px',
|
width: styles.screen.width + 'px',
|
||||||
height: styles.screen.height + 'px',
|
height: styles.screen.height + 'px',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}, [videoURL, playerContext.player.screen.document])
|
}, [playerContext.player.screen.document]);
|
||||||
return (
|
|
||||||
<div />
|
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 />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(ReplayWindow);
|
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',
|
||||||
|
height: styles.screen.height + 'px',
|
||||||
|
margin: styles.margin,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
milliseconds: number
|
||||||
valueOf: () => number
|
valueOf: () => number
|
||||||
}
|
}
|
||||||
|
videoURL: string[]
|
||||||
domURL: string[]
|
domURL: string[]
|
||||||
devtoolsURL: string[]
|
devtoolsURL: string[]
|
||||||
/** deprecated */
|
/** deprecated */
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,40 @@
|
||||||
import * as fzstd from 'fzstd';
|
import * as fzstd from 'fzstd';
|
||||||
import { gunzipSync } from 'fflate'
|
import { gunzipSync } from 'fflate';
|
||||||
|
|
||||||
const unpack = (b: Uint8Array): Uint8Array => {
|
const unpack = (b: Uint8Array): Uint8Array => {
|
||||||
// zstd magical numbers 40 181 47 253
|
// zstd magical numbers 40 181 47 253
|
||||||
const isZstd = b[0] === 0x28 && b[1] === 0xb5 && b[2] === 0x2f && b[3] === 0xfd
|
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 isGzip = b[0] === 0x1f && b[1] === 0x8b && b[2] === 0x08;
|
||||||
|
let data = b;
|
||||||
if (isGzip) {
|
if (isGzip) {
|
||||||
const now = performance.now()
|
const now = performance.now();
|
||||||
const data = gunzipSync(b)
|
const uData = gunzipSync(b);
|
||||||
console.debug(
|
console.debug(
|
||||||
"Gunzip time",
|
'Gunzip time',
|
||||||
Math.floor(performance.now() - now) + 'ms',
|
Math.floor(performance.now() - now) + 'ms',
|
||||||
'size',
|
'size',
|
||||||
Math.floor(b.byteLength / 1024),
|
Math.floor(b.byteLength / 1024),
|
||||||
'->',
|
'->',
|
||||||
Math.floor(data.byteLength / 1024),
|
Math.floor(uData.byteLength / 1024),
|
||||||
'kb'
|
'kb'
|
||||||
)
|
);
|
||||||
return data
|
data = uData;
|
||||||
}
|
}
|
||||||
if (isZstd) {
|
if (isZstd) {
|
||||||
const now = performance.now()
|
const now = performance.now();
|
||||||
const data = fzstd.decompress(b)
|
const uData = fzstd.decompress(b);
|
||||||
console.debug(
|
console.debug(
|
||||||
"Zstd unpack time",
|
'Zstd unpack time',
|
||||||
Math.floor(performance.now() - now) + 'ms',
|
Math.floor(performance.now() - now) + 'ms',
|
||||||
'size',
|
'size',
|
||||||
Math.floor(b.byteLength / 1024),
|
Math.floor(b.byteLength / 1024),
|
||||||
'->',
|
'->',
|
||||||
Math.floor(data.byteLength / 1024),
|
Math.floor(uData.byteLength / 1024),
|
||||||
'kb'
|
'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 logger from 'App/logger';
|
||||||
import { getResourceFromNetworkRequest } from "Player";
|
import { TarFile } from "js-untar";
|
||||||
|
import { getResourceFromNetworkRequest } from 'Player';
|
||||||
|
|
||||||
import type { Store } from 'Player';
|
import type { Store } from 'Player';
|
||||||
import { IMessageManager } from 'Player/player/Animator';
|
import { IMessageManager } from 'Player/player/Animator';
|
||||||
|
|
@ -11,43 +12,52 @@ import Lists, {
|
||||||
INITIAL_STATE as LISTS_INITIAL_STATE,
|
INITIAL_STATE as LISTS_INITIAL_STATE,
|
||||||
State as ListsState,
|
State as ListsState,
|
||||||
} from './IOSLists';
|
} from './IOSLists';
|
||||||
import IOSPerformanceTrackManager, { PerformanceChartPoint } from "Player/mobile/managers/IOSPerformanceTrackManager";
|
import IOSPerformanceTrackManager, {
|
||||||
|
PerformanceChartPoint,
|
||||||
|
} from 'Player/mobile/managers/IOSPerformanceTrackManager';
|
||||||
import { MType } from '../web/messages';
|
import { MType } from '../web/messages';
|
||||||
import type { Message } from '../web/messages';
|
import type { Message } from '../web/messages';
|
||||||
|
import SnapshotManager from 'Player/mobile/managers/SnapshotManager';
|
||||||
|
|
||||||
import Screen, {
|
import Screen, {
|
||||||
INITIAL_STATE as SCREEN_INITIAL_STATE,
|
INITIAL_STATE as SCREEN_INITIAL_STATE,
|
||||||
State as ScreenState,
|
State as ScreenState,
|
||||||
} from '../web/Screen/Screen';
|
} from '../web/Screen/Screen';
|
||||||
|
|
||||||
import { Log } from './types/log'
|
import { Log } from './types/log';
|
||||||
|
|
||||||
import type { SkipInterval } from '../web/managers/ActivityManager';
|
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 = {
|
const perfWarningFrustrations = {
|
||||||
thermalState: {
|
thermalState: {
|
||||||
title: "Overheating",
|
title: 'Overheating',
|
||||||
icon: "thermometer-sun",
|
icon: 'thermometer-sun',
|
||||||
},
|
},
|
||||||
memoryWarning: {
|
memoryWarning: {
|
||||||
title: "High Memory Usage",
|
title: 'High Memory Usage',
|
||||||
icon: "memory-ios"
|
icon: 'memory-ios',
|
||||||
},
|
},
|
||||||
lowDiskSpace: {
|
lowDiskSpace: {
|
||||||
title: "Low Disk Space",
|
title: 'Low Disk Space',
|
||||||
icon: "low-disc-space"
|
icon: 'low-disc-space',
|
||||||
},
|
},
|
||||||
isLowPowerModeEnabled: {
|
isLowPowerModeEnabled: {
|
||||||
title: "Low Power Mode",
|
title: 'Low Power Mode',
|
||||||
icon: "battery-charging"
|
icon: 'battery-charging',
|
||||||
},
|
},
|
||||||
batteryLevel: {
|
batteryLevel: {
|
||||||
title: "Low Battery",
|
title: 'Low Battery',
|
||||||
icon: "battery"
|
icon: 'battery',
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface State extends ScreenState, ListsState {
|
export interface State extends ScreenState, ListsState {
|
||||||
skipIntervals: SkipInterval[];
|
skipIntervals: SkipInterval[];
|
||||||
|
|
@ -64,9 +74,15 @@ export interface State extends ScreenState, ListsState {
|
||||||
messagesProcessed: boolean;
|
messagesProcessed: boolean;
|
||||||
eventCount: number;
|
eventCount: number;
|
||||||
updateWarnings: 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 {
|
export default class IOSMessageManager implements IMessageManager {
|
||||||
static INITIAL_STATE: State = {
|
static INITIAL_STATE: State = {
|
||||||
|
|
@ -83,6 +99,7 @@ export default class IOSMessageManager implements IMessageManager {
|
||||||
lastMessageTime: 0,
|
lastMessageTime: 0,
|
||||||
messagesProcessed: false,
|
messagesProcessed: false,
|
||||||
messagesLoading: false,
|
messagesLoading: false,
|
||||||
|
currentSnapshot: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
private activityManager: ActivityManager | null = null;
|
private activityManager: ActivityManager | null = null;
|
||||||
|
|
@ -92,6 +109,7 @@ export default class IOSMessageManager implements IMessageManager {
|
||||||
private lastMessageTime: number = 0;
|
private lastMessageTime: number = 0;
|
||||||
private touchManager: TouchManager;
|
private touchManager: TouchManager;
|
||||||
private lists: Lists;
|
private lists: Lists;
|
||||||
|
public snapshotManager: SnapshotManager;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly session: Record<string, any>,
|
private readonly session: Record<string, any>,
|
||||||
|
|
@ -104,6 +122,7 @@ export default class IOSMessageManager implements IMessageManager {
|
||||||
this.lists = new Lists(initialLists);
|
this.lists = new Lists(initialLists);
|
||||||
this.touchManager = new TouchManager(screen);
|
this.touchManager = new TouchManager(screen);
|
||||||
this.activityManager = new ActivityManager(this.session.duration.milliseconds); // only if not-live
|
this.activityManager = new ActivityManager(this.session.duration.milliseconds); // only if not-live
|
||||||
|
this.snapshotManager = new SnapshotManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateDimensions(dimensions: { width: number; height: number }) {
|
public updateDimensions(dimensions: { width: number; height: number }) {
|
||||||
|
|
@ -111,16 +130,16 @@ export default class IOSMessageManager implements IMessageManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateLists(lists: Partial<InitialLists>) {
|
public updateLists(lists: Partial<InitialLists>) {
|
||||||
const exceptions = lists.exceptions
|
const exceptions = lists.exceptions;
|
||||||
exceptions?.forEach(e => {
|
exceptions?.forEach((e) => {
|
||||||
this.lists.lists.exceptions.insert(e);
|
this.lists.lists.exceptions.insert(e);
|
||||||
this.lists.lists.log.insert(e)
|
this.lists.lists.log.insert(e);
|
||||||
})
|
});
|
||||||
lists.frustrations?.forEach(f => {
|
lists.frustrations?.forEach((f) => {
|
||||||
this.lists.lists.frustrations.insert(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();
|
const currentState = this.state.get();
|
||||||
this.state.update({
|
this.state.update({
|
||||||
eventCount: currentState.eventCount + eventCount,
|
eventCount: currentState.eventCount + eventCount,
|
||||||
|
|
@ -134,8 +153,8 @@ export default class IOSMessageManager implements IMessageManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getListsFullState = () => {
|
public getListsFullState = () => {
|
||||||
return this.lists.getFullListsState();
|
return this.lists.getFullListsState();
|
||||||
}
|
};
|
||||||
|
|
||||||
private waitingForFiles: boolean = false;
|
private waitingForFiles: boolean = false;
|
||||||
public onFileReadSuccess = () => {
|
public onFileReadSuccess = () => {
|
||||||
|
|
@ -144,29 +163,29 @@ export default class IOSMessageManager implements IMessageManager {
|
||||||
eventCount: this.lists?.lists.event?.length || 0,
|
eventCount: this.lists?.lists.event?.length || 0,
|
||||||
performanceChartData: this.performanceManager.chartData,
|
performanceChartData: this.performanceManager.chartData,
|
||||||
...this.lists.getFullListsState(),
|
...this.lists.getFullListsState(),
|
||||||
}
|
};
|
||||||
|
|
||||||
if (this.activityManager) {
|
if (this.activityManager) {
|
||||||
this.activityManager.end();
|
this.activityManager.end();
|
||||||
newState['skipIntervals'] = this.activityManager.list
|
newState['skipIntervals'] = this.activityManager.list;
|
||||||
}
|
}
|
||||||
this.state.update(newState);
|
this.state.update(newState);
|
||||||
};
|
};
|
||||||
|
|
||||||
public onFileReadFailed = (...e: any[]) => {
|
public onFileReadFailed = (...e: any[]) => {
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
this.state.update({error: true});
|
this.state.update({ error: true });
|
||||||
this.uiErrorHandler?.error('Error requesting a session file');
|
this.uiErrorHandler?.error('Error requesting a session file');
|
||||||
};
|
};
|
||||||
|
|
||||||
public onFileReadFinally = () => {
|
public onFileReadFinally = () => {
|
||||||
this.waitingForFiles = false;
|
this.waitingForFiles = false;
|
||||||
this.state.update({messagesProcessed: true});
|
this.state.update({ messagesProcessed: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
public startLoading = () => {
|
public startLoading = () => {
|
||||||
this.waitingForFiles = true;
|
this.waitingForFiles = true;
|
||||||
this.state.update({messagesProcessed: false});
|
this.state.update({ messagesProcessed: false });
|
||||||
this.setMessagesLoading(true);
|
this.setMessagesLoading(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -182,7 +201,7 @@ export default class IOSMessageManager implements IMessageManager {
|
||||||
if (lastPerformanceTrackMessage) {
|
if (lastPerformanceTrackMessage) {
|
||||||
Object.assign(stateToUpdate, {
|
Object.assign(stateToUpdate, {
|
||||||
performanceChartTime: lastPerformanceTrackMessage.time,
|
performanceChartTime: lastPerformanceTrackMessage.time,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.touchManager.move(t);
|
this.touchManager.move(t);
|
||||||
|
|
@ -194,61 +213,67 @@ export default class IOSMessageManager implements IMessageManager {
|
||||||
this.setMessagesLoading(true);
|
this.setMessagesLoading(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(stateToUpdate, this.lists.moveGetState(t))
|
const snapshot = this.snapshotManager.moveReady(t);
|
||||||
Object.assign(stateToUpdate, { performanceListNow: this.lists.lists.performance.listNow })
|
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);
|
Object.keys(stateToUpdate).length > 0 && this.state.update(stateToUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
distributeMessage = (msg: Message & { tabId: string }): void => {
|
distributeMessage = (msg: Message & { tabId: string }): void => {
|
||||||
const lastMessageTime = Math.max(msg.time, this.lastMessageTime);
|
const lastMessageTime = Math.max(msg.time, this.lastMessageTime);
|
||||||
this.lastMessageTime = lastMessageTime;
|
this.lastMessageTime = lastMessageTime;
|
||||||
this.state.update({lastMessageTime});
|
this.state.update({ lastMessageTime });
|
||||||
if (userEvents.includes(msg.tp)) {
|
if (userEvents.includes(msg.tp)) {
|
||||||
this.activityManager?.updateAcctivity(msg.time);
|
this.activityManager?.updateAcctivity(msg.time);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (msg.tp) {
|
switch (msg.tp) {
|
||||||
case MType.IosPerformanceEvent:
|
case MType.IosPerformanceEvent:
|
||||||
const performanceStats = ['background', 'memoryUsage', 'mainThreadCPU']
|
const performanceStats = ['background', 'memoryUsage', 'mainThreadCPU'];
|
||||||
if (performanceStats.includes(msg.name)) {
|
if (performanceStats.includes(msg.name)) {
|
||||||
this.performanceManager.append(msg);
|
this.performanceManager.append(msg);
|
||||||
}
|
}
|
||||||
if (performanceWarnings.includes(msg.name)) {
|
if (performanceWarnings.includes(msg.name)) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const item = perfWarningFrustrations[msg.name]
|
const item = perfWarningFrustrations[msg.name];
|
||||||
this.lists.lists.performance.append({
|
this.lists.lists.performance.append({
|
||||||
...msg,
|
...msg,
|
||||||
name: item.title,
|
name: item.title,
|
||||||
techName: msg.name,
|
techName: msg.name,
|
||||||
icon: item.icon,
|
icon: item.icon,
|
||||||
type: 'ios_perf_event'
|
type: 'ios_perf_event',
|
||||||
} as any)
|
} as any);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// case MType.IosInputEvent:
|
// case MType.IosInputEvent:
|
||||||
// console.log('input', msg)
|
// console.log('input', msg)
|
||||||
// break;
|
// break;
|
||||||
case MType.IosNetworkCall:
|
case MType.IosNetworkCall:
|
||||||
this.lists.lists.fetch.insert(getResourceFromNetworkRequest(msg, this.sessionStart))
|
this.lists.lists.fetch.insert(getResourceFromNetworkRequest(msg, this.sessionStart));
|
||||||
break;
|
break;
|
||||||
case MType.WsChannel:
|
case MType.WsChannel:
|
||||||
this.lists.lists.websocket.insert(msg)
|
this.lists.lists.websocket.insert(msg);
|
||||||
break;
|
break;
|
||||||
case MType.IosEvent:
|
case MType.IosEvent:
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.lists.lists.event.insert({...msg, source: 'openreplay'});
|
this.lists.lists.event.insert({ ...msg, source: 'openreplay' });
|
||||||
break;
|
break;
|
||||||
case MType.IosSwipeEvent:
|
case MType.IosSwipeEvent:
|
||||||
case MType.IosClickEvent:
|
case MType.IosClickEvent:
|
||||||
this.touchManager.append(msg);
|
this.touchManager.append(msg);
|
||||||
break;
|
break;
|
||||||
case MType.IosLog:
|
case MType.IosLog:
|
||||||
const log = {...msg, level: msg.severity}
|
const log = { ...msg, level: msg.severity };
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.lists.lists.log.append(Log(log));
|
this.lists.lists.log.append(Log(log));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log(msg)
|
console.log(msg);
|
||||||
// stuff
|
// stuff
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -257,16 +282,17 @@ export default class IOSMessageManager implements IMessageManager {
|
||||||
setMessagesLoading = (messagesLoading: boolean) => {
|
setMessagesLoading = (messagesLoading: boolean) => {
|
||||||
this.screen.display(!messagesLoading);
|
this.screen.display(!messagesLoading);
|
||||||
// @ts-ignore idk
|
// @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 }) {
|
private setSize({ height, width }: { height: number; width: number }) {
|
||||||
this.screen.scale({height, width});
|
this.screen.scale({ height, width });
|
||||||
this.state.update({width, height});
|
this.state.update({ width, height });
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: clean managers?
|
// TODO: clean managers?
|
||||||
clean() {
|
clean() {
|
||||||
|
this.snapshotManager?.clean();
|
||||||
this.state.update(IOSMessageManager.INITIAL_STATE);
|
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 type { Store } from 'Player';
|
||||||
import MessageLoader from "Player/web/MessageLoader";
|
import MessageLoader from 'Player/web/MessageLoader';
|
||||||
import Player from '../player/Player'
|
import Player from '../player/Player';
|
||||||
import Screen, { ScaleMode } from '../web/Screen/Screen'
|
import Screen, { ScaleMode } from '../web/Screen/Screen';
|
||||||
import IOSMessageManager from "Player/mobile/IOSMessageManager";
|
import IOSMessageManager from 'Player/mobile/IOSMessageManager';
|
||||||
|
|
||||||
|
export const PlayerMode = {
|
||||||
|
VIDEO: 'video',
|
||||||
|
SNAPS: 'snaps',
|
||||||
|
};
|
||||||
|
|
||||||
export default class IOSPlayer extends Player {
|
export default class IOSPlayer extends Player {
|
||||||
static readonly INITIAL_STATE = {
|
static readonly INITIAL_STATE = {
|
||||||
|
|
@ -12,120 +17,137 @@ export default class IOSPlayer extends Player {
|
||||||
...MessageLoader.INITIAL_STATE,
|
...MessageLoader.INITIAL_STATE,
|
||||||
...IOSMessageManager.INITIAL_STATE,
|
...IOSMessageManager.INITIAL_STATE,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
}
|
mode: null,
|
||||||
public screen: Screen
|
autoplay: false,
|
||||||
protected messageManager: IOSMessageManager
|
};
|
||||||
protected readonly messageLoader: MessageLoader
|
public screen: Screen;
|
||||||
|
protected messageManager: IOSMessageManager;
|
||||||
|
protected readonly messageLoader: MessageLoader;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected wpState: Store<any>,
|
protected wpState: Store<any>,
|
||||||
session: SessionFilesInfo,
|
session: SessionFilesInfo,
|
||||||
public readonly uiErrorHandler?: { error: (msg: string) => void }
|
public readonly uiErrorHandler?: { error: (msg: string) => void }
|
||||||
) {
|
) {
|
||||||
const screen = new Screen(true, ScaleMode.Embed)
|
const hasTar = session.videoURL.some((url) => url.includes('.tar.'));
|
||||||
const messageManager = new IOSMessageManager(session, wpState, screen, uiErrorHandler)
|
const screen = new Screen(true, ScaleMode.Embed);
|
||||||
|
const messageManager = new IOSMessageManager(session, wpState, screen, uiErrorHandler);
|
||||||
const messageLoader = new MessageLoader(
|
const messageLoader = new MessageLoader(
|
||||||
session,
|
session,
|
||||||
wpState,
|
wpState,
|
||||||
messageManager,
|
messageManager,
|
||||||
false,
|
false,
|
||||||
uiErrorHandler
|
uiErrorHandler
|
||||||
)
|
);
|
||||||
super(wpState, messageManager);
|
super(wpState, messageManager);
|
||||||
this.screen = screen
|
this.pause()
|
||||||
this.messageManager = messageManager
|
this.screen = screen;
|
||||||
this.messageLoader = messageLoader
|
this.messageManager = messageManager;
|
||||||
|
this.messageLoader = messageLoader;
|
||||||
|
|
||||||
void messageLoader.loadFiles()
|
if (hasTar) {
|
||||||
const endTime = session.duration?.valueOf() || 0
|
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({
|
wpState.update({
|
||||||
session,
|
session,
|
||||||
endTime,
|
endTime,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
attach = (parent: HTMLElement) => {
|
attach = (parent: HTMLElement) => {
|
||||||
this.screen.attach(parent)
|
this.screen.attach(parent);
|
||||||
}
|
};
|
||||||
|
|
||||||
public updateDimensions(dimensions: { width: number; height: number }) {
|
public updateDimensions(dimensions: { width: number; height: number }) {
|
||||||
return this.messageManager.updateDimensions(dimensions)
|
return this.messageManager.updateDimensions(dimensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateLists(session: any) {
|
public updateLists(session: any) {
|
||||||
const exceptions = session.crashes.concat(session.errors || [])
|
const exceptions = session.crashes.concat(session.errors || []);
|
||||||
const lists = {
|
const lists = {
|
||||||
event: session.events.map((e: Record<string, any>) => {
|
event:
|
||||||
if (e.name === 'Click') e.name = 'Touch'
|
session.events.map((e: Record<string, any>) => {
|
||||||
return e
|
if (e.name === 'Click') e.name = 'Touch';
|
||||||
}) || [],
|
return e;
|
||||||
|
}) || [],
|
||||||
frustrations: session.frustrations || [],
|
frustrations: session.frustrations || [],
|
||||||
stack: session.stackEvents || [],
|
stack: session.stackEvents || [],
|
||||||
exceptions: exceptions.map(({ name, ...rest }: any) =>
|
exceptions:
|
||||||
Log({
|
exceptions.map(({ name, ...rest }: any) =>
|
||||||
level: LogLevel.ERROR,
|
Log({
|
||||||
value: name,
|
level: LogLevel.ERROR,
|
||||||
name,
|
value: name,
|
||||||
message: rest.reason,
|
name,
|
||||||
errorId: rest.crashId || rest.errorId,
|
message: rest.reason,
|
||||||
...rest,
|
errorId: rest.crashId || rest.errorId,
|
||||||
})
|
...rest,
|
||||||
) || [],
|
})
|
||||||
}
|
) || [],
|
||||||
|
};
|
||||||
|
|
||||||
return this.messageManager.updateLists(lists)
|
return this.messageManager.updateLists(lists);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateOverlayStyle(style: Partial<CSSStyleDeclaration>) {
|
public updateOverlayStyle(style: Partial<CSSStyleDeclaration>) {
|
||||||
this.screen.updateOverlayStyle(style)
|
this.screen.updateOverlayStyle(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
injectPlayer = (player: HTMLElement) => {
|
injectPlayer = (player: HTMLElement) => {
|
||||||
this.screen.addToBody(player)
|
this.screen.addToBody(player);
|
||||||
this.screen.addMobileStyles()
|
this.screen.addMobileStyles();
|
||||||
window.addEventListener('resize', () =>
|
window.addEventListener('resize', () =>
|
||||||
this.customScale(this.customConstrains.width, this.customConstrains.height)
|
this.customScale(this.customConstrains.width, this.customConstrains.height)
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
scale = () => {
|
scale = () => {
|
||||||
// const { width, height } = this.wpState.get()
|
// const { width, height } = this.wpState.get()
|
||||||
if (!this.screen) return;
|
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
|
// sometimes happens in live assist sessions for some reason
|
||||||
this.screen?.scale?.(this.customConstrains)
|
this.screen?.scale?.(this.customConstrains);
|
||||||
}
|
};
|
||||||
|
|
||||||
customConstrains = {
|
customConstrains = {
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
}
|
};
|
||||||
customScale = (width: number, height: number) => {
|
customScale = (width: number, height: number) => {
|
||||||
if (!this.screen) return;
|
if (!this.screen) return;
|
||||||
this.screen?.scale?.({ width, height })
|
this.screen?.scale?.({ width, height });
|
||||||
this.customConstrains = { width, height }
|
this.customConstrains = { width, height };
|
||||||
this.wpState.update({ scale: this.screen.getScale() })
|
this.wpState.update({ scale: this.screen.getScale() });
|
||||||
}
|
};
|
||||||
|
|
||||||
addFullscreenBoundary = (isFullscreen?: boolean) => {
|
addFullscreenBoundary = (isFullscreen?: boolean) => {
|
||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
this.screen?.addFullscreenBoundary()
|
this.screen?.addFullscreenBoundary();
|
||||||
} else {
|
} else {
|
||||||
this.screen?.addMobileStyles()
|
this.screen?.addMobileStyles();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
clean = () => {
|
clean = () => {
|
||||||
super.clean()
|
super.clean();
|
||||||
this.screen.clean()
|
this.screen.clean();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.screen = undefined;
|
this.screen = undefined;
|
||||||
this.messageLoader.clean()
|
this.messageLoader.clean();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.messageManager = undefined;
|
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 { decryptSessionBytes } from './network/crypto';
|
||||||
import MFileReader from './messages/MFileReader';
|
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 logger from 'App/logger';
|
||||||
import unpack from 'Player/common/unpack';
|
import unpack from 'Player/common/unpack';
|
||||||
import MessageManager from 'Player/web/MessageManager';
|
import MessageManager from 'Player/web/MessageManager';
|
||||||
import IOSMessageManager from 'Player/mobile/IOSMessageManager';
|
import IOSMessageManager from 'Player/mobile/IOSMessageManager';
|
||||||
|
import unpackTar from 'Player/common/tarball';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
firstFileLoading: boolean;
|
firstFileLoading: boolean;
|
||||||
|
|
@ -79,6 +80,18 @@ export default class MessageLoader {
|
||||||
this.messageManager.setMessagesLoading(false);
|
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>) {
|
async loadDomFiles(urls: string[], parser: (b: Uint8Array) => Promise<void>) {
|
||||||
if (urls.length > 0) {
|
if (urls.length > 0) {
|
||||||
this.store.update({ domLoading: true });
|
this.store.update({ domLoading: true });
|
||||||
|
|
@ -116,10 +129,10 @@ export default class MessageLoader {
|
||||||
this.messageManager.startLoading();
|
this.messageManager.startLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.loadMobs()
|
await this.loadMobs();
|
||||||
} catch (sessionLoadError) {
|
} catch (sessionLoadError) {
|
||||||
try {
|
try {
|
||||||
await this.loadEFSMobs()
|
await this.loadEFSMobs();
|
||||||
} catch (unprocessedLoadError) {
|
} catch (unprocessedLoadError) {
|
||||||
this.messageManager.onFileReadFailed(sessionLoadError, unprocessedLoadError);
|
this.messageManager.onFileReadFailed(sessionLoadError, unprocessedLoadError);
|
||||||
}
|
}
|
||||||
|
|
@ -129,35 +142,35 @@ export default class MessageLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMobs = async () => {
|
loadMobs = async () => {
|
||||||
const loadMethod =
|
const loadMethod =
|
||||||
this.session.domURL && this.session.domURL.length > 0
|
this.session.domURL && this.session.domURL.length > 0
|
||||||
? {
|
? {
|
||||||
mobUrls: this.session.domURL,
|
mobUrls: this.session.domURL,
|
||||||
parser: () => this.createNewParser(true, this.processMessages, 'dom'),
|
parser: () => this.createNewParser(true, this.processMessages, 'dom'),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
mobUrls: this.session.mobsUrl,
|
mobUrls: this.session.mobsUrl,
|
||||||
parser: () => this.createNewParser(false, this.processMessages, 'dom'),
|
parser: () => this.createNewParser(false, this.processMessages, 'dom'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const parser = loadMethod.parser();
|
const parser = loadMethod.parser();
|
||||||
const devtoolsParser = this.createNewParser(true, this.processMessages, 'devtools');
|
const devtoolsParser = this.createNewParser(true, this.processMessages, 'devtools');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* to speed up time to replay
|
* to speed up time to replay
|
||||||
* we load first dom mob file before the rest
|
* we load first dom mob file before the rest
|
||||||
* (because parser can read them in parallel)
|
* (because parser can read them in parallel)
|
||||||
* as a tradeoff we have some copy-paste code
|
* as a tradeoff we have some copy-paste code
|
||||||
* for the devtools file
|
* for the devtools file
|
||||||
* */
|
* */
|
||||||
await loadFiles([loadMethod.mobUrls[0]], parser);
|
await loadFiles([loadMethod.mobUrls[0]], parser);
|
||||||
const restDomFilesPromise = this.loadDomFiles([...loadMethod.mobUrls.slice(1)], parser);
|
const restDomFilesPromise = this.loadDomFiles([...loadMethod.mobUrls.slice(1)], parser);
|
||||||
const restDevtoolsFilesPromise = this.loadDevtools(devtoolsParser);
|
const restDevtoolsFilesPromise = this.loadDevtools(devtoolsParser);
|
||||||
|
|
||||||
await Promise.allSettled([restDomFilesPromise, restDevtoolsFilesPromise]);
|
await Promise.allSettled([restDomFilesPromise, restDevtoolsFilesPromise]);
|
||||||
this.messageManager.onFileReadSuccess();
|
this.messageManager.onFileReadSuccess();
|
||||||
}
|
};
|
||||||
|
|
||||||
loadEFSMobs = async () => {
|
loadEFSMobs = async () => {
|
||||||
this.store.update({ domLoading: true, devtoolsLoading: true });
|
this.store.update({ domLoading: true, devtoolsLoading: true });
|
||||||
|
|
@ -172,16 +185,16 @@ export default class MessageLoader {
|
||||||
const devtoolsParser = this.createNewParser(false, this.processMessages, 'devtoolsEFS');
|
const devtoolsParser = this.createNewParser(false, this.processMessages, 'devtoolsEFS');
|
||||||
const parseDomPromise: Promise<any> =
|
const parseDomPromise: Promise<any> =
|
||||||
domData.status === 'fulfilled'
|
domData.status === 'fulfilled'
|
||||||
? domParser(domData.value)
|
? domParser(domData.value)
|
||||||
: Promise.reject('No dom file in EFS');
|
: Promise.reject('No dom file in EFS');
|
||||||
const parseDevtoolsPromise: Promise<any> =
|
const parseDevtoolsPromise: Promise<any> =
|
||||||
devtoolsData.status === 'fulfilled'
|
devtoolsData.status === 'fulfilled'
|
||||||
? devtoolsParser(devtoolsData.value)
|
? devtoolsParser(devtoolsData.value)
|
||||||
: Promise.reject('No devtools file in EFS');
|
: Promise.reject('No devtools file in EFS');
|
||||||
|
|
||||||
await Promise.all([parseDomPromise, parseDevtoolsPromise]);
|
await Promise.all([parseDomPromise, parseDevtoolsPromise]);
|
||||||
this.messageManager.onFileReadSuccess();
|
this.messageManager.onFileReadSuccess();
|
||||||
}
|
};
|
||||||
|
|
||||||
clean() {
|
clean() {
|
||||||
this.store.update(MessageLoader.INITIAL_STATE);
|
this.store.update(MessageLoader.INITIAL_STATE);
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,8 @@ export default class Screen {
|
||||||
if (this.document) {
|
if (this.document) {
|
||||||
this.document.body.style.margin = '0';
|
this.document.body.style.margin = '0';
|
||||||
this.document.body.appendChild(el);
|
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")
|
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) {
|
async function requestEFSMobFile(filename: string) {
|
||||||
const api = new APIClient()
|
const api = new APIClient()
|
||||||
const res = await api.fetch('/unprocessed/' + filename)
|
const res = await api.fetch('/unprocessed/' + filename)
|
||||||
|
|
@ -58,13 +68,13 @@ async function requestEFSMobFile(filename: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const processAPIStreamResponse = (response: Response, skippable: boolean) => {
|
const processAPIStreamResponse = (response: Response, skippable: boolean) => {
|
||||||
return new Promise<Blob>((res, rej) => {
|
return new Promise<ArrayBuffer>((res, rej) => {
|
||||||
if (response.status === 404 && skippable) {
|
if (response.status === 404 && skippable) {
|
||||||
return rej(ALLOWED_404)
|
return rej(ALLOWED_404)
|
||||||
}
|
}
|
||||||
if (response.status >= 400) {
|
if (response.status >= 400) {
|
||||||
return rej(`Bad file status code ${response.status}. Url: ${response.url}`)
|
return rej(`Bad file status code ${response.status}. Url: ${response.url}`)
|
||||||
}
|
}
|
||||||
res(response.blob())
|
res(response.arrayBuffer())
|
||||||
}).then(async blob => new Uint8Array(await blob.arrayBuffer()))
|
}).then(async buf => new Uint8Array(buf))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"immutable": "^4.0.0-rc.12",
|
"immutable": "^4.0.0-rc.12",
|
||||||
"jest-environment-jsdom": "^29.5.0",
|
"jest-environment-jsdom": "^29.5.0",
|
||||||
|
"js-untar": "^2.0.0",
|
||||||
"jsbi": "^4.1.0",
|
"jsbi": "^4.1.0",
|
||||||
"jshint": "^2.11.1",
|
"jshint": "^2.11.1",
|
||||||
"jspdf": "^2.5.1",
|
"jspdf": "^2.5.1",
|
||||||
|
|
|
||||||
|
|
@ -15261,6 +15261,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"js-yaml@npm:^3.13.1":
|
||||||
version: 3.14.1
|
version: 3.14.1
|
||||||
resolution: "js-yaml@npm:3.14.1"
|
resolution: "js-yaml@npm:3.14.1"
|
||||||
|
|
@ -18152,6 +18159,7 @@ __metadata:
|
||||||
immutable: ^4.0.0-rc.12
|
immutable: ^4.0.0-rc.12
|
||||||
jest: ^29.5.0
|
jest: ^29.5.0
|
||||||
jest-environment-jsdom: ^29.5.0
|
jest-environment-jsdom: ^29.5.0
|
||||||
|
js-untar: ^2.0.0
|
||||||
jsbi: ^4.1.0
|
jsbi: ^4.1.0
|
||||||
jshint: ^2.11.1
|
jshint: ^2.11.1
|
||||||
jspdf: ^2.5.1
|
jspdf: ^2.5.1
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue