Commit 17d316f3 authored by suily's avatar suily
Browse files

Initial commit

parents
Pipeline #3368 failed with stages
in 0 seconds
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Logger from '@/common/logger/Logger';
import {
CacheConfig,
Environment,
FetchFunction,
GraphQLResponse,
LogEvent,
Network,
ObservableFromValue,
RecordSource,
RequestParameters,
Store,
UploadableMap,
Variables,
} from 'relay-runtime';
import fetchGraphQL from './fetchGraphQL';
function createFetchRelay(endpoint: string): FetchFunction {
return (
request: RequestParameters,
variables: Variables,
cacheConfig: CacheConfig,
uploadables?: UploadableMap | null,
): ObservableFromValue<GraphQLResponse> => {
Logger.debug(
`fetching query ${request.name} with ${JSON.stringify(variables)}`,
);
return fetchGraphQL(endpoint, request, variables, cacheConfig, uploadables);
};
}
export function createEnvironment(endpoint: string): Environment {
return new Environment({
log: (logEvent: LogEvent) => Logger.debug(logEvent.name, logEvent),
network: Network.create(createFetchRelay(endpoint)),
store: new Store(new RecordSource()),
});
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ErrorFallback from '@/common/error/ErrorFallback';
import LoadingMessage from '@/common/loading/LoadingMessage';
import {createEnvironment} from '@/graphql/RelayEnvironment';
import {
ComponentType,
PropsWithChildren,
ReactNode,
Suspense,
useMemo,
useState,
} from 'react';
import {ErrorBoundary, FallbackProps} from 'react-error-boundary';
import {RelayEnvironmentProvider} from 'react-relay';
type Props = PropsWithChildren<{
suspenseFallback?: ReactNode;
errorFallback?: ComponentType<FallbackProps>;
endpoint: string;
}>;
export default function OnevisionRelayEnvironmentProvider({
suspenseFallback,
errorFallback = ErrorFallback,
endpoint,
children,
}: Props) {
const [retryKey, setRetryKey] = useState<number>(0);
const environment = useMemo(() => {
return createEnvironment(endpoint);
// The retryKey is needed to force a new Relay Environment
// instance when the user retries after an error occurred.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [endpoint, retryKey]);
// Force re-creating Relay Environment
function handleReset() {
setRetryKey(k => k + 1);
}
return (
<ErrorBoundary onReset={handleReset} FallbackComponent={errorFallback}>
<RelayEnvironmentProvider environment={environment}>
<Suspense fallback={suspenseFallback ?? <LoadingMessage />}>
{children}
</Suspense>
</RelayEnvironmentProvider>
</ErrorBoundary>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default class CreateFilmstripError extends Error {
override name = 'CreateFilmstripError';
constructor(message?: string) {
super(message);
}
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default class DrawFrameError extends Error {
override name = 'DrawFrameError';
constructor(message?: string) {
super(message);
}
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default class WebGLContextError extends Error {
override name = 'WebGLContextError';
constructor(message?: string) {
super(message);
}
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Logger from '@/common/logger/Logger';
import {
CacheConfig,
GraphQLResponse,
RequestParameters,
UploadableMap,
Variables,
} from 'relay-runtime';
/**
* Inspired by https://github.com/facebook/relay/issues/1844
*/
export default async function fetchGraphQL(
endpoint: string,
request: RequestParameters,
variables: Variables,
cacheConfig: CacheConfig,
uploadables?: UploadableMap | null,
): Promise<GraphQLResponse> {
const url = `${endpoint}/graphql`;
const headers: {[name: string]: string} = {};
const requestInit: RequestInit = {
method: 'POST',
headers,
credentials: 'include',
};
const customHeaders = (cacheConfig?.metadata?.headers ?? {}) as {
[key: string]: string;
};
requestInit.headers = Object.assign(customHeaders, requestInit.headers);
if (uploadables != null) {
const formData = new FormData();
formData.append(
'operations',
JSON.stringify({
query: request.text,
variables,
}),
);
const uploadableMap: {
[key: string]: string[];
} = {};
Object.keys(uploadables).forEach(key => {
uploadableMap[key] = [`variables.${key}`];
});
formData.append('map', JSON.stringify(uploadableMap));
Object.keys(uploadables).forEach(key => {
formData.append(key, uploadables[key]);
});
requestInit.body = formData;
} else {
requestInit.headers = Object.assign(
{'Content-Type': 'application/json'},
requestInit.headers,
);
requestInit.body = JSON.stringify({
query: request.text,
variables,
});
}
try {
const response = await fetch(url, requestInit);
const result = await response.json();
// Handle any intentional GraphQL errors, which are passed through the
// errors property in the JSON payload.
if ('errors' in result) {
for (const error of result.errors) {
Logger.error(error);
}
}
return result;
} catch (error) {
Logger.error(`Could not connect to GraphQL endpoint ${url}`, error);
throw error;
}
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class DataArray {
data: Uint8Array;
readonly shape: number[];
constructor(data: Uint8Array, shape: Array<number>) {
this.data = data;
this.shape = shape;
}
}
export type RLEObject = {
size: [h: number, w: number];
counts: string;
};
type RLE = {
h: number;
w: number;
m: number;
cnts: number[];
};
type BB = number[];
function rleInit(R: RLE, h: number, w: number, m: number, cnts: number[]) {
R.h = h;
R.w = w;
R.m = m;
R.cnts = m === 0 ? [0] : cnts;
}
function rlesInit(R: RLE[], n: number) {
let i;
for (i = 0; i < n; i++) {
R[i] = {h: 0, w: 0, m: 0, cnts: [0]};
rleInit(R[i], 0, 0, 0, [0]);
}
}
class RLEs {
_R: RLE[];
_n: number;
constructor(n: number) {
this._R = [];
rlesInit(this._R, n);
this._n = n;
}
}
export class Masks {
_mask: Uint8Array;
_h: number;
_w: number;
_n: number;
constructor(h: number, w: number, n: number) {
this._mask = new Uint8Array(h * w * n);
this._h = h;
this._w = w;
this._n = n;
}
toDataArray(): DataArray {
return new DataArray(this._mask, [this._h, this._w, this._n]);
}
}
// encode mask to RLEs objects
// list of RLE string can be generated by RLEs member function
export function encode(mask: DataArray): RLEObject[] {
const h = mask.shape[0];
const w = mask.shape[1];
const n = mask.shape[2];
const Rs = new RLEs(n);
rleEncode(Rs._R, mask.data, h, w, n);
const objs = _toString(Rs);
return objs;
}
// decode mask from compressed list of RLE string or RLEs object
export function decode(rleObjs: RLEObject[]): DataArray {
const Rs = _frString(rleObjs);
const h = Rs._R[0].h;
const w = Rs._R[0].w;
const n = Rs._n;
const masks = new Masks(h, w, n);
rleDecode(Rs._R, masks._mask, n);
return masks.toDataArray();
}
export function toBbox(rleObjs: RLEObject[]): BB {
const Rs = _frString(rleObjs);
const n = Rs._n;
const bb: BB = [];
rleToBbox(Rs._R, bb, n);
return bb;
}
function rleEncode(R: RLE[], M: Uint8Array, h: number, w: number, n: number) {
let i;
let j;
let k;
const a = w * h;
let c;
const cnts: number[] = [];
let p;
for (i = 0; i < n; i++) {
const from = a * i;
const to = a * (i + 1);
// Slice data for current RLE object
const T = M.slice(from, to);
k = 0;
p = 0;
c = 0;
for (j = 0; j < a; j++) {
if (T[j] !== p) {
cnts[k++] = c;
c = 0;
p = T[j];
}
c++;
}
cnts[k++] = c;
rleInit(R[i], h, w, k, [...cnts]);
}
}
function rleDecode(R: RLE[], M: Uint8Array, n: number): void {
let i;
let j;
let k;
let p = 0;
for (i = 0; i < n; i++) {
let v = false;
for (j = 0; j < R[i].m; j++) {
for (k = 0; k < R[i].cnts[j]; k++) {
M[p++] = v === false ? 0 : 1;
}
v = !v;
}
}
}
function rleToString(R: RLE): string {
/* Similar to LEB128 but using 6 bits/char and ascii chars 48-111. */
let i;
const m = R.m;
let p = 0;
let x: number;
let more;
const s: string[] = [];
for (i = 0; i < m; i++) {
x = R.cnts[i];
if (i > 2) {
x -= R.cnts[i - 2];
}
more = true; // 1;
while (more) {
let c = x & 0x1f;
x >>= 5;
more = c & 0x10 ? x != -1 : x != 0;
if (more) {
c |= 0x20;
}
c += 48;
s[p++] = String.fromCharCode(c);
}
}
return s.join('');
}
// internal conversion from Python RLEs object to compressed RLE format
function _toString(Rs: RLEs): RLEObject[] {
const n = Rs._n;
let py_string;
let c_string;
const objs: RLEObject[] = [];
for (let i = 0; i < n; i++) {
c_string = rleToString(Rs._R[i]);
py_string = c_string;
objs.push({
size: [Rs._R[i].h, Rs._R[i].w],
counts: py_string,
});
}
return objs;
}
// internal conversion from compressed RLE format to Python RLEs object
function _frString(rleObjs: RLEObject[]): RLEs {
const n = rleObjs.length;
const Rs = new RLEs(n);
let py_string;
let c_string;
for (let i = 0; i < rleObjs.length; i++) {
const obj = rleObjs[i];
py_string = obj.counts;
c_string = py_string;
rleFrString(Rs._R[i], c_string, obj.size[0], obj.size[1]);
}
return Rs;
}
function rleToBbox(R: RLE[], bb: BB, n: number) {
for (let i = 0; i < n; i++) {
const h = R[i].h;
const w = R[i].w;
let m = R[i].m;
// The RLE structure likely contains run-length encoded data where each
// element represents a count of consecutive pixels with the same value in
// a binary image (black or white). Since the counts represent both black
// and white pixels, this operation ((siz)(m/2)) * 2 is used to ensure that
// m is always an even number. By doing so, the code can later check
// whether the current pixel is black or white based on whether the index j
// is even or odd.
m = Math.floor(m / 2) * 2;
let xs = w;
let ys = h;
let xe = 0;
let ye = 0;
let cc = 0;
let t;
let y;
let x;
let xp = 0;
if (m === 0) {
bb[4 * i] = bb[4 * i + 1] = bb[4 * i + 2] = bb[4 * i + 3] = 0;
continue;
}
for (let j = 0; j < m; j++) {
cc += R[i].cnts[j];
t = cc - (j % 2);
y = t % h;
x = Math.floor((t - y) / h);
if (j % 2 === 0) {
xp = x;
} else if (xp < x) {
ys = 0;
ye = h - 1;
}
xs = Math.min(xs, x);
xe = Math.max(xe, x);
ys = Math.min(ys, y);
ye = Math.max(ye, y);
}
bb[4 * i] = xs;
bb[4 * i + 2] = xe - xs + 1;
bb[4 * i + 1] = ys;
bb[4 * i + 3] = ye - ys + 1;
}
}
function rleFrString(R: RLE, s: string, h: number, w: number): void {
let m = 0;
let p = 0;
let k;
let x;
let more;
let cnts = [];
while (s[m]) {
m++;
}
cnts = [];
m = 0;
while (s[p]) {
x = 0;
k = 0;
more = 1;
while (more) {
const c = s.charCodeAt(p) - 48;
x |= (c & 0x1f) << (5 * k);
more = c & 0x20;
p++;
k++;
if (!more && c & 0x10) {
x |= -1 << (5 * k);
}
}
if (m > 2) {
x += cnts[m - 2];
}
cnts[m++] = x;
}
rleInit(R, h, w, m, cnts);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {spacing} from '@/theme/tokens.stylex';
import stylex from '@stylexjs/stylex';
import {PropsWithChildren} from 'react';
type Props = PropsWithChildren;
const styles = stylex.create({
container: {
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'stretch',
alignItems: 'stretch',
gap: spacing[12],
paddingHorizontal: spacing[12],
paddingVertical: spacing[4],
'@media screen and (max-width: 768px)': {
display: 'flex',
flexDirection: 'column-reverse',
gap: 0,
marginTop: spacing[0],
marginBottom: spacing[0],
paddingHorizontal: spacing[0],
paddingBottom: spacing[0],
},
},
});
export default function DemoPageLayout({children}: Props) {
return <div {...stylex.props(styles.container)}>{children}</div>;
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import LoadingStateScreen from '@/common/loading/LoadingStateScreen';
import useSettingsContext from '@/settings/useSettingsContext';
import {Cog6ToothIcon} from '@heroicons/react/24/outline';
import stylex from '@stylexjs/stylex';
import {Suspense} from 'react';
import {Button, Indicator} from 'react-daisyui';
import {Outlet} from 'react-router-dom';
const styles = stylex.create({
container: {
display: 'flex',
flexDirection: 'column',
height: '100%',
maxHeight: '100vh',
backgroundColor: '#000',
},
content: {
position: 'relative',
flex: '1 1 0%',
display: 'flex',
flexDirection: 'column',
overflowX: 'auto',
overflowY: {
default: 'auto',
'@media screen and (max-width: 768px)': 'auto',
},
},
debugActions: {
display: 'flex',
flexDirection: 'column',
position: 'fixed',
top: 100,
right: 0,
backgroundColor: 'white',
borderRadius: 3,
},
});
export default function RootLayout() {
const {openModal, hasChanged} = useSettingsContext();
return (
<div {...stylex.props(styles.container)}>
<div {...stylex.props(styles.content)}>
<Suspense
fallback={
<LoadingStateScreen
title="Loading demo..."
description="This may take a few moments, you're almost there!"
/>
}>
<Outlet />
</Suspense>
</div>
<div {...stylex.props(styles.debugActions)}>
<Indicator>
{hasChanged && (
<Indicator.Item
className="badge badge-primary scale-50"
horizontal="start"
vertical="top"
/>
)}
<Button
color="ghost"
onClick={openModal}
shape="circle"
size="xs"
startIcon={<Cog6ToothIcon className="w-4 h-4" />}
title="Bugnub"
/>
</Indicator>
</div>
</div>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import App from '@/App.tsx';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Toolbar from '@/common/components/toolbar/Toolbar';
import DemoVideoEditor from '@/common/components/video/editor/DemoVideoEditor';
import useInputVideo from '@/common/components/video/useInputVideo';
import StatsView from '@/debug/stats/StatsView';
import {VideoData} from '@/demo/atoms';
import DemoPageLayout from '@/layouts/DemoPageLayout';
import {DemoPageQuery} from '@/routes/__generated__/DemoPageQuery.graphql';
import {useEffect, useMemo} from 'react';
import {graphql, useLazyLoadQuery} from 'react-relay';
import {Location, useLocation} from 'react-router-dom';
type LocationState = {
video?: VideoData;
};
export default function DemoPage() {
const {state} = useLocation() as Location<LocationState>;
const data = useLazyLoadQuery<DemoPageQuery>(
graphql`
query DemoPageQuery {
defaultVideo {
path
posterPath
url
posterUrl
height
width
}
}
`,
{},
);
const {setInputVideo} = useInputVideo();
const video = useMemo(() => {
return state?.video ?? data.defaultVideo;
}, [state, data]);
useEffect(() => {
setInputVideo(video);
}, [video, setInputVideo]);
return (
<DemoPageLayout>
<StatsView />
<Toolbar />
<DemoVideoEditor video={video} />
</DemoPageLayout>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import LoadingStateScreen from '@/common/loading/LoadingStateScreen';
import DemoPage from '@/routes/DemoPage';
import stylex from '@stylexjs/stylex';
import {isFirefox} from 'react-device-detect';
const styles = stylex.create({
link: {
textDecorationLine: 'underline',
color: '#A7B3BF',
},
});
const REQUIRED_WINDOW_APIS = ['VideoEncoder', 'VideoDecoder', 'VideoFrame'];
function isBrowserSupported() {
for (const api of REQUIRED_WINDOW_APIS) {
if (!(api in window)) {
return false;
}
}
// Test if transferControlToOffscreen is supported. For example, this will
// fail on iOS version < 16.4
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/transferControlToOffscreen
const canvas = document.createElement('canvas');
if (typeof canvas.transferControlToOffscreen !== 'function') {
return false;
}
return true;
}
export default function DemoPageWrapper() {
const isBrowserUnsupported = !isBrowserSupported();
if (isBrowserUnsupported && isFirefox) {
const nightlyUrl = 'https://wiki.mozilla.org/Nightly';
return (
<LoadingStateScreen
title="Sorry Firefox!"
description={
<div>
This version of Firefox doesn’t support the video features we’ll
need to run this demo. You can either update Firefox to the latest
nightly build{' '}
<a
{...stylex.props(styles.link)}
href={nightlyUrl}
target="_blank"
rel="noreferrer">
here
</a>
, or try again using Chrome or Safari.
</div>
}
linkProps={{to: '..', label: 'Back to homepage'}}
/>
);
}
if (isBrowserUnsupported) {
return (
<LoadingStateScreen
title="Uh oh, this browser isn’t supported."
description="This browser doesn’t support the video features we’ll need to run this demo. Try again using Chrome, Safari, or Firefox Nightly."
linkProps={{to: '..', label: 'Back to homepage'}}
/>
);
}
return <DemoPage />;
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import LoadingStateScreen from '@/common/loading/LoadingStateScreen';
export default function PageNotFoundPage() {
return (
<LoadingStateScreen
title="Page not found"
description="It looks like you might be in the wrong place."
linkProps={{
to: '..',
label: 'Click here to access the SAM 2 Demo',
}}
/>
);
}
/**
* @generated SignedSource<<f457eacd20a61cba601921caee2a18f5>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Query } from 'relay-runtime';
export type DemoPageQuery$variables = Record<PropertyKey, never>;
export type DemoPageQuery$data = {
readonly defaultVideo: {
readonly height: number;
readonly path: string;
readonly posterPath: string | null | undefined;
readonly posterUrl: string;
readonly url: string;
readonly width: number;
};
};
export type DemoPageQuery = {
response: DemoPageQuery$data;
variables: DemoPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"alias": null,
"args": null,
"concreteType": "Video",
"kind": "LinkedField",
"name": "defaultVideo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "path",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "posterPath",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "url",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "posterUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "height",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "width",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "DemoPageQuery",
"selections": (v0/*: any*/),
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "DemoPageQuery",
"selections": (v0/*: any*/)
},
"params": {
"cacheID": "71cbafce4d2d047acdc54d86504f2d2e",
"id": null,
"metadata": {},
"name": "DemoPageQuery",
"operationKind": "query",
"text": "query DemoPageQuery {\n defaultVideo {\n path\n posterPath\n url\n posterUrl\n height\n width\n }\n}\n"
}
};
})();
(node as any).hash = "63c9465d78b30d42d6fc11e50a9af142";
export default node;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Tooltip from '@/common/components/Tooltip';
import {ArrowPathIcon, CheckIcon, XMarkIcon} from '@heroicons/react/24/solid';
import {ChangeEvent, KeyboardEvent, useEffect, useMemo, useState} from 'react';
import {Button, Form, Input, Join} from 'react-daisyui';
type Props<T extends string | number> = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'size' | 'color' | 'onChange'
> & {
label: string;
defaultValue: T;
initialValue: T;
onChange: (value: string) => void;
};
function getStep(value: number) {
const stringValue = String(value);
const decimals = stringValue.split('.')[1];
if (decimals != null) {
// Not using 0.1 ** decimals.length because this will result in rounding
// errors, e.g., 0.1 ** 2 => 0.010000000000000002.
return 1 / 10 ** decimals.length;
}
return 1;
}
export default function ApprovableInput<T extends string | number>({
label,
defaultValue,
initialValue,
onChange,
...otherProps
}: Props<T>) {
const [value, setValue] = useState<string>(`${initialValue}`);
useEffect(() => {
setValue(`${initialValue}`);
}, [initialValue]);
const step = useMemo(() => {
return typeof defaultValue === 'number' && isFinite(defaultValue)
? getStep(defaultValue)
: undefined;
}, [defaultValue]);
return (
<div>
<Form.Label className="flex-col items-start gap-2" title={label}>
<Join className="w-full">
<Input
{...otherProps}
className="w-full join-item"
value={value}
step={step}
placeholder={`${defaultValue}`}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
}}
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault();
onChange(value);
}
}}
/>
<Tooltip message="Reset to default">
<Button
className="join-item"
onClick={event => {
event.preventDefault();
setValue(`${defaultValue}`);
}}>
<ArrowPathIcon className="h-4 w-4" />
</Button>
</Tooltip>
<Tooltip message="Revert change">
<Button
className="join-item"
color="neutral"
disabled={initialValue == value}
onClick={event => {
event.preventDefault();
setValue(`${initialValue}`);
}}>
<XMarkIcon className="h-4 w-4" />
</Button>
</Tooltip>
<Tooltip message="Apply change">
<Button
className="join-item"
color="primary"
disabled={initialValue == value}
onClick={event => {
event.preventDefault();
onChange(value);
}}>
<CheckIcon className="h-4 w-4" />
</Button>
</Tooltip>
</Join>
</Form.Label>
</div>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {INFERENCE_API_ENDPOINT, VIDEO_API_ENDPOINT} from '@/demo/DemoConfig';
import ApprovableInput from '@/settings/ApprovableInput';
import useSettingsContext from '@/settings/useSettingsContext';
export default function SAMVSettings() {
const {settings, dispatch} = useSettingsContext();
return (
<div>
<ApprovableInput
label="Video API Endpoint"
defaultValue={VIDEO_API_ENDPOINT}
initialValue={settings.videoAPIEndpoint}
onChange={url => dispatch({type: 'change-video-api-endpoint', url})}
/>
<ApprovableInput
label="Inference API Endpoint"
defaultValue={INFERENCE_API_ENDPOINT}
initialValue={settings.inferenceAPIEndpoint}
onChange={url => dispatch({type: 'change-inference-api-endpoint', url})}
/>
</div>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import emptyFunction from '@/common/utils/emptyFunction';
import {INFERENCE_API_ENDPOINT, VIDEO_API_ENDPOINT} from '@/demo/DemoConfig';
import SettingsModal from '@/settings/SettingsModal';
import {
Action,
DEFAULT_SETTINGS,
Settings,
settingsReducer,
} from '@/settings/SettingsReducer';
import {
PropsWithChildren,
createContext,
useCallback,
useMemo,
useRef,
} from 'react';
import {useImmerReducer} from 'use-immer';
type ContextProps = {
settings: Settings;
dispatch: React.Dispatch<Action>;
openModal: () => void;
closeModal: () => void;
hasChanged: boolean;
};
export const SettingsContext = createContext<ContextProps>({
settings: DEFAULT_SETTINGS,
dispatch: emptyFunction,
openModal: emptyFunction,
closeModal: emptyFunction,
hasChanged: false,
});
type Props = PropsWithChildren;
export default function SettingsContextProvider({children}: Props) {
const [state, dispatch] = useImmerReducer(
settingsReducer,
DEFAULT_SETTINGS,
settings => {
// Load the settings from local storage. Eventually use the reducer init
// to handle initial loading.
return settingsReducer(settings, {type: 'load-state'});
},
);
const modalRef = useRef<HTMLDialogElement>(null);
const openModal = useCallback(() => {
modalRef.current?.showModal();
}, [modalRef]);
const handleCloseModal = useCallback(() => {
modalRef.current?.close();
}, [modalRef]);
const hasChanged = useMemo(() => {
return (
VIDEO_API_ENDPOINT !== state.videoAPIEndpoint ||
INFERENCE_API_ENDPOINT !== state.inferenceAPIEndpoint
);
}, [state.videoAPIEndpoint, state.inferenceAPIEndpoint]);
const value = useMemo(
() => ({
settings: state,
dispatch,
openModal,
closeModal: handleCloseModal,
hasChanged,
}),
[state, dispatch, openModal, handleCloseModal, hasChanged],
);
return (
<SettingsContext.Provider value={value}>
{children}
<SettingsModal ref={modalRef} />
</SettingsContext.Provider>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {DEMO_FRIENDLY_NAME} from '@/demo/DemoConfig';
import SAM2Settings from '@/settings/SAM2Settings';
import {XMarkIcon} from '@heroicons/react/24/solid';
import {forwardRef, useState} from 'react';
import {Button, Modal} from 'react-daisyui';
import useSettingsContext from './useSettingsContext';
type Props = unknown;
type Config = {
key: 'sam2';
title: string;
component: React.ElementType;
};
const SettingsConfig: Config[] = [
{
key: 'sam2',
title: DEMO_FRIENDLY_NAME,
component: SAM2Settings,
},
];
export default forwardRef<HTMLDialogElement, Props>(
function SettingsModal(_props, ref) {
const {closeModal} = useSettingsContext();
const [activeConfig, setActiveConfig] = useState<Config>(SettingsConfig[0]);
const SettingsComponent = activeConfig.component;
return (
<Modal
data-testid="settings-modal"
ref={ref}
className="lg:absolute lg:top-10 lg:w-11/12 lg:max-w-4xl flex flex-col"
responsive={true}>
<Button
size="sm"
color="ghost"
shape="circle"
className="absolute right-2 top-2"
startIcon={<XMarkIcon className="w-6 h-6" />}
onClick={closeModal}
/>
<Modal.Header className="font-bold">Settings</Modal.Header>
<Modal.Body className="flex flex-col grow overflow-hidden">
<div className="flex flex-col md:lg:flex-row gap-4 md:lg:gap-12 overflow-hidden">
<div className="flex flex-row shrink-0 md:lg:flex-col gap-4 md:lg:py-2 overflow-x-auto">
{SettingsConfig.map(config => (
<div
key={config.key}
data-testid={`show-settings-${config.key}`}
className={`cursor-pointer whitespace-nowrap ${
activeConfig.key === config.key && 'text-primary'
} ${
activeConfig.key === config.key &&
'sm:underline md:lg:no-underline sm:underline-offset-4'
}`}
onClick={() => setActiveConfig(config)}>
{config.title}
</div>
))}
</div>
<div
data-testid={`settings-${activeConfig.key}`}
className="overflow-hidden overflow-y-auto grow md:lg:pt-2">
<div className="flex flex-col grow-0 flex-1">
<h1 className="hidden md:lg:block">{activeConfig.title}</h1>
<SettingsComponent />
</div>
</div>
</div>
</Modal.Body>
<Modal.Actions className="shrink-0">
<Button onClick={closeModal}>Close</Button>
</Modal.Actions>
</Modal>
);
},
);
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {INFERENCE_API_ENDPOINT, VIDEO_API_ENDPOINT} from '@/demo/DemoConfig';
export type Settings = {
videoAPIEndpoint: string;
inferenceAPIEndpoint: string;
};
// Key used to store the settings in the browser's local storage.
export const SAM2_SETTINGS_KEY = 'SAM2_SETTINGS_KEY';
export type Action =
| {type: 'load-state'}
| {type: 'change-video-api-endpoint'; url: string}
| {type: 'change-inference-api-endpoint'; url: string};
export const DEFAULT_SETTINGS: Settings = {
videoAPIEndpoint: VIDEO_API_ENDPOINT,
inferenceAPIEndpoint: INFERENCE_API_ENDPOINT,
};
export function settingsReducer(state: Settings, action: Action): Settings {
function storeSettings(newState: Settings): void {
localStorage.setItem(SAM2_SETTINGS_KEY, JSON.stringify(newState));
}
switch (action.type) {
case 'load-state': {
try {
const serializedSettings = localStorage.getItem(SAM2_SETTINGS_KEY);
if (serializedSettings != null) {
return JSON.parse(serializedSettings) as Settings;
} else {
// Store default settings in local storage. This will populate the
// settings in the local storage on first app load or when user
// cleared the browser cache.
storeSettings(state);
}
} catch {
// Could not parse settings. Using default settings instead.
}
return state;
}
case 'change-video-api-endpoint':
state.videoAPIEndpoint = action.url;
break;
case 'change-inference-api-endpoint':
state.inferenceAPIEndpoint = action.url;
break;
}
// Store the settings state on every change
storeSettings(state);
return state;
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {useContext} from 'react';
import {SettingsContext} from '@/settings/SettingsContextProvider';
export default function useSettingsContext() {
return useContext(SettingsContext);
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment