Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
Bw-bestperf
SAM2
Commits
17d316f3
Commit
17d316f3
authored
Feb 04, 2026
by
suily
Browse files
Initial commit
parents
Pipeline
#3368
failed with stages
in 0 seconds
Changes
959
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
1407 additions
and
0 deletions
+1407
-0
demo/frontend/src/App.tsx
demo/frontend/src/App.tsx
+33
-0
demo/frontend/src/assets/icons/angery.png
demo/frontend/src/assets/icons/angery.png
+0
-0
demo/frontend/src/assets/icons/heart.png
demo/frontend/src/assets/icons/heart.png
+0
-0
demo/frontend/src/assets/icons/whistle.png
demo/frontend/src/assets/icons/whistle.png
+0
-0
demo/frontend/src/assets/scss/App.scss
demo/frontend/src/assets/scss/App.scss
+374
-0
demo/frontend/src/assets/videos/sam2_720px_dark.mp4
demo/frontend/src/assets/videos/sam2_720px_dark.mp4
+0
-0
demo/frontend/src/assets/videos/sam2_video_poster.png
demo/frontend/src/assets/videos/sam2_video_poster.png
+0
-0
demo/frontend/src/common/codecs/VideoDecoder.ts
demo/frontend/src/common/codecs/VideoDecoder.ts
+284
-0
demo/frontend/src/common/codecs/VideoEncoder.ts
demo/frontend/src/common/codecs/VideoEncoder.ts
+139
-0
demo/frontend/src/common/codecs/WebCodecUtils.ts
demo/frontend/src/common/codecs/WebCodecUtils.ts
+50
-0
demo/frontend/src/common/components/MobileFirstClickBanner.tsx
...frontend/src/common/components/MobileFirstClickBanner.tsx
+74
-0
demo/frontend/src/common/components/Tooltip.tsx
demo/frontend/src/common/components/Tooltip.tsx
+41
-0
demo/frontend/src/common/components/annotations/AddObjectButton.tsx
...end/src/common/components/annotations/AddObjectButton.tsx
+49
-0
demo/frontend/src/common/components/annotations/ClearAllPointsInVideoButton.tsx
...on/components/annotations/ClearAllPointsInVideoButton.tsx
+81
-0
demo/frontend/src/common/components/annotations/CloseSessionButton.tsx
.../src/common/components/annotations/CloseSessionButton.tsx
+38
-0
demo/frontend/src/common/components/annotations/FirstClickView.tsx
...tend/src/common/components/annotations/FirstClickView.tsx
+49
-0
demo/frontend/src/common/components/annotations/LimitNotice.tsx
...rontend/src/common/components/annotations/LimitNotice.tsx
+30
-0
demo/frontend/src/common/components/annotations/MobileObjectsList.tsx
...d/src/common/components/annotations/MobileObjectsList.tsx
+78
-0
demo/frontend/src/common/components/annotations/MobileObjectsToolbar.tsx
...rc/common/components/annotations/MobileObjectsToolbar.tsx
+51
-0
demo/frontend/src/common/components/annotations/MobileObjectsToolbarHeader.tsx
...mon/components/annotations/MobileObjectsToolbarHeader.tsx
+36
-0
No files found.
demo/frontend/src/App.tsx
0 → 100644
View file @
17d316f3
/**
* 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
SAM2DemoApp
from
'
@/demo/SAM2DemoApp
'
;
import
SettingsContextProvider
from
'
@/settings/SettingsContextProvider
'
;
import
{
RouterProvider
,
createBrowserRouter
}
from
'
react-router-dom
'
;
export
default
function
App
()
{
const
router
=
createBrowserRouter
([
{
path
:
'
*
'
,
element
:
(
<
SettingsContextProvider
>
<
SAM2DemoApp
/>
</
SettingsContextProvider
>
),
},
]);
return
<
RouterProvider
router
=
{
router
}
/>;
}
demo/frontend/src/assets/icons/angery.png
0 → 100644
View file @
17d316f3
15.6 KB
demo/frontend/src/assets/icons/heart.png
0 → 100644
View file @
17d316f3
19 KB
demo/frontend/src/assets/icons/whistle.png
0 → 100644
View file @
17d316f3
14.4 KB
demo/frontend/src/assets/scss/App.scss
0 → 100644
View file @
17d316f3
/**
* 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.
*/
@tailwind
base
;
@tailwind
components
;
@tailwind
utilities
;
.tab
{
display
:
flex
;
padding
:
0px
0px
;
margin-right
:
6px
;
align-items
:
center
;
height
:
100%
;
}
@layer
base
{
@font-face
{
font-family
:
'Inter'
;
src
:
url(/fonts/Inter-VariableFont.ttf)
format
(
'truetype-variations'
);
}
}
body
{
font-family
:
'Inter'
,
sans-serif
;
-webkit-font-smoothing
:
antialiased
;
-moz-osx-font-smoothing
:
grayscale
;
}
body
,
html
,
#root
{
height
:
100%
;
@media
screen
and
(
max-width
:
'768px'
)
{
overflow
:
hidden
;
}
}
:root
{
--segEv-font
:
'Inter'
,
system-ui
,
-
apple-system
,
BlinkMacSystemFont
,
'Segoe UI'
,
Oxygen
,
Ubuntu
,
Cantarell
,
'Open Sans'
,
'Helvetica Neue'
,
sans-serif
;
--perspective
:
4000px
;
color-scheme
:
dark
;
}
h1
,
h2
,
h3
,
h4
,
h5
,
h6
{
font-family
:
'Inter'
,
sans-serif
;
}
.prose
.display
h1
{
@apply
text-4xl
text-gray-800
font-medium
leading-tight
;
}
.prose
.display
h2
{
@apply
text-gray-800
font-medium
leading-tight
;
font-size
:
2
.5rem
;
}
.prose
h1
{
@apply
text-3xl
text-gray-800
font-medium
leading-tight
mt-2
mb-4
;
letter-spacing
:
0
.016rem
;
}
.prose
h2
{
@apply
text-2xl
text-gray-800
font-medium
leading-tight
my-2
;
letter-spacing
:
0
.01rem
;
}
.prose
h3
{
@apply
text-xl
text-gray-800
font-medium
leading-tight
my-2
;
letter-spacing
:
0
.005rem
;
}
.prose
h4
{
@apply
text-lg
text-gray-800
font-medium
leading-tight
my-2
;
}
.prose
h5
{
@apply
text-xl
text-gray-700
font-normal
leading-normal
my-2
;
letter-spacing
:
0
.005rem
;
}
.prose
h6
{
@apply
text-base
text-gray-700
font-normal
leading-normal
my-2
;
}
.prose
p
{
@apply
text-sm
text-gray-700
font-normal
leading-normal
;
@apply
leading-snug
;
}
.prose
ol
,
.prose
ul
{
@apply
text-sm
text-gray-700
font-normal
leading-normal
;
padding-right
:
2rem
;
}
.dark-mode
h1
,
.dark-mode
h2
,
.dark-mode
h3
,
.dark-mode
h4
,
.dark-mode
h5
,
.dark-mode
p
,
.dark-mode
ol
,
.dark-mode
ul
,
.dark-mode
p
*,
.dark-mode
ol
*,
.dark-mode
ul
*,
≈
{
@apply
text-white
;
}
.
dark-mode
h4
,
.
dark-mode
h6
,
.
dark-mode
li
:
:
marker
,
.
dark-mode
a
{
@apply
text-gray-200
;
}
.flex-grow-2
{
flex-grow
:
2
;
}
.flex-grow-3
{
flex-grow
:
3
;
}
.flex-grow-4
{
flex-grow
:
4
;
}
.flex-grow-5
{
flex-grow
:
5
;
}
.nav-title
{
font-family
:
var
(
--
segEv-font
);
}
.segment-active
{
animation
:
segment-highlight
2s
linear
infinite
;
stroke-dasharray
:
5
,
10
;
stroke-width
:
4px
;
}
@keyframes
segment-highlight
{
to
{
stroke-dashoffset
:
60
;
}
}
.segment-select
{
animation
:
segment-dotted
2s
linear
infinite
;
stroke-dasharray
:
3
,
5
;
stroke-width
:
3px
;
}
@keyframes
segment-dotted
{
to
{
stroke-dashoffset
:
24
;
}
}
/**
* Daisy UI customizations
*/
.btn
{
@apply
normal-case
rounded-md
;
}
.comp_summary
h1
,
.comp_summary
h2
,
.comp_summary
h3
{
@apply
mb-4
;
}
.disabled
{
opacity
:
0
.4
;
pointer-events
:
none
;
}
.absolute-center
{
top
:
50%
;
left
:
50%
;
transform
:
translate
(
-50%
,
-50%
);
}
@screen
lg
{
.drawer
.grid
{
grid-template-columns
:
max-content
1fr
;
}
}
.fade-in
{
transition
:
opacity
0
.5s
;
opacity
:
1
!
important
;
}
.react-photo-gallery--gallery
>
div
{
gap
:
0
.25rem
;
}
.sticker
{
filter
:
drop-shadow
(
0
.25rem
0
.25rem
5px
#fff
)
drop-shadow
(
-0
.25rem
0
.25rem
5px
#fff
)
drop-shadow
(
0
.25rem
-0
.25rem
5px
#fff
)
drop-shadow
(
-0
.25rem
-0
.25rem
5px
#fff
);
transition
:
filter
0
.3s
ease-out
;
}
.sticker
:hover
,
.sticker-select
{
filter
:
drop-shadow
(
0
.25rem
0
.25rem
1px
#2962d9
)
drop-shadow
(
-0
.25rem
0
.25rem
1px
#2962d9
)
drop-shadow
(
0
.25rem
-0
.25rem
1px
#2962d9
)
drop-shadow
(
-0
.25rem
-0
.25rem
1px
#2962d9
);
}
/* keyframe animations */
.mask-path
,
.reveal
{
opacity
:
0
;
animation
:
reveal
0
.4s
ease-in
forwards
;
}
.slow-reveal
{
animation
:
reveal
1s
ease-in
;
}
.reveal-then-conceal
{
opacity
:
0
;
animation
:
reveal-then-conceal
1
.5s
ease-in-out
forwards
;
}
@keyframes
reveal
{
from
{
opacity
:
0
;
}
to
{
opacity
:
1
;
}
}
@keyframes
reveal-then-conceal
{
from
{
opacity
:
0
;
}
50
%
{
opacity
:
1
;
}
to
{
opacity
:
0
;
}
}
.background-animate
{
background-size
:
400%
;
animation
:
pulse
3s
ease
infinite
;
}
@keyframes
pulse
{
0
%
,
100
%
{
background-position
:
0%
50%
;
}
50
%
{
background-position
:
100%
50%
;
}
}
/* Fix for Safari and Mobile Safari:
Extracted Tailwind progress-bar styles and applied
them to a <div> instead of a <progress> element */
.loading-bar
{
position
:
relative
;
width
:
100%
;
-webkit-appearance
:
none
;
-moz-appearance
:
none
;
appearance
:
none
;
overflow
:
hidden
;
height
:
0
.5rem
;
border-radius
:
1rem
;
border-radius
:
var
(
--
rounded-box
,
1rem
);
vertical-align
:
baseline
;
background-color
:
hsl
(
var
(
--
n
)
/
var
(
--
tw-bg-opacity
));
--tw-bg-opacity
:
0
.2
;
&
::after
{
--tw-bg-opacity
:
1
;
background-color
:
hsl
(
var
(
--
n
)
/
var
(
--
tw-bg-opacity
));
content
:
''
;
position
:
absolute
;
top
:
0px
;
bottom
:
0px
;
left
:
-40%
;
width
:
33
.333333%
;
border-radius
:
1rem
;
border-radius
:
var
(
--
rounded-box
,
1rem
);
animation
:
loading
5s
infinite
ease-in-out
;
}
}
@keyframes
loading
{
50
%
{
left
:
107%
;
}
}
@keyframes
inAnimation
{
0
%
{
opacity
:
0
;
max-height
:
0px
;
}
50
%
{
opacity
:
1
;
}
100
%
{
opacity
:
1
;
max-height
:
600px
;
}
}
@keyframes
outAnimation
{
0
%
{
opacity
:
1
;
max-height
:
600px
;
}
50
%
{
opacity
:
0
;
}
100
%
{
opacity
:
0
;
max-height
:
0px
;
}
}
@keyframes
ellipsisAnimation
{
0
%
{
content
:
''
;
}
25
%
{
content
:
'.'
;
}
50
%
{
content
:
'..'
;
}
75
%
{
content
:
'...'
;
}
}
.
ellipsis
:
:
after
{
content
:
''
;
animation
:
ellipsisAnimation
1
.5s
infinite
;
}
demo/frontend/src/assets/videos/sam2_720px_dark.mp4
0 → 100644
View file @
17d316f3
File added
demo/frontend/src/assets/videos/sam2_video_poster.png
0 → 100644
View file @
17d316f3
8.56 KB
demo/frontend/src/common/codecs/VideoDecoder.ts
0 → 100644
View file @
17d316f3
/**
* 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
{
cloneFrame
}
from
'
@/common/codecs/WebCodecUtils
'
;
import
{
FileStream
}
from
'
@/common/utils/FileUtils
'
;
import
{
createFile
,
DataStream
,
MP4ArrayBuffer
,
MP4File
,
MP4Sample
,
MP4VideoTrack
,
}
from
'
mp4box
'
;
import
{
isAndroid
,
isChrome
,
isEdge
,
isWindows
}
from
'
react-device-detect
'
;
export
type
ImageFrame
=
{
bitmap
:
VideoFrame
;
timestamp
:
number
;
duration
:
number
;
};
export
type
DecodedVideo
=
{
width
:
number
;
height
:
number
;
frames
:
ImageFrame
[];
numFrames
:
number
;
fps
:
number
;
};
function
decodeInternal
(
identifier
:
string
,
onReady
:
(
mp4File
:
MP4File
)
=>
Promise
<
void
>
,
onProgress
:
(
decodedVideo
:
DecodedVideo
)
=>
void
,
):
Promise
<
DecodedVideo
>
{
return
new
Promise
((
resolve
,
reject
)
=>
{
const
imageFrames
:
ImageFrame
[]
=
[];
const
globalSamples
:
MP4Sample
[]
=
[];
let
decoder
:
VideoDecoder
;
let
track
:
MP4VideoTrack
|
null
=
null
;
const
mp4File
=
createFile
();
mp4File
.
onError
=
reject
;
mp4File
.
onReady
=
async
info
=>
{
if
(
info
.
videoTracks
.
length
>
0
)
{
track
=
info
.
videoTracks
[
0
];
}
else
{
// The video does not have a video track, so looking if there is an
// "otherTracks" available. Note, I couldn't find any documentation
// about "otherTracks" in WebCodecs [1], but it was available in the
// info for MP4V-ES, which isn't supported by Chrome [2].
// However, we'll still try to get the track and then throw an error
// further down in the VideoDecoder.isConfigSupported if the codec is
// not supported by the browser.
//
// [1] https://www.w3.org/TR/webcodecs/
// [2] https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_codecs#mp4v-es
track
=
info
.
otherTracks
[
0
];
}
if
(
track
==
null
)
{
reject
(
new
Error
(
`
${
identifier
}
does not contain a video track`
));
return
;
}
const
timescale
=
track
.
timescale
;
const
edits
=
track
.
edits
;
let
frame_n
=
0
;
decoder
=
new
VideoDecoder
({
// Be careful with any await in this function. The VideoDecoder will
// not await output and continue calling it with decoded frames.
async
output
(
inputFrame
)
{
if
(
track
==
null
)
{
reject
(
new
Error
(
`
${
identifier
}
does not contain a video track`
));
return
;
}
const
saveTrack
=
track
;
// If the track has edits, we'll need to check that only frames are
// returned that are within the edit list. This can happen for
// trimmed videos that have not been transcoded and therefore the
// video track contains more frames than those visually rendered when
// playing back the video.
if
(
edits
!=
null
&&
edits
.
length
>
0
)
{
const
cts
=
Math
.
round
(
(
inputFrame
.
timestamp
*
timescale
)
/
1
_000_000
,
);
if
(
cts
<
edits
[
0
].
media_time
)
{
inputFrame
.
close
();
return
;
}
}
// Workaround for Chrome where the decoding stops at ~17 frames unless
// the VideoFrame is closed. So, the workaround here is to create a
// new VideoFrame and close the decoded VideoFrame.
// The frame has to be cloned, or otherwise some frames at the end of the
// video will be black. Note, the default VideoFrame.clone doesn't work
// and it is using a frame cloning found here:
// https://webcodecs-blogpost-demo.glitch.me/
if
(
(
isAndroid
&&
isChrome
)
||
(
isWindows
&&
isChrome
)
||
(
isWindows
&&
isEdge
)
)
{
const
clonedFrame
=
await
cloneFrame
(
inputFrame
);
inputFrame
.
close
();
inputFrame
=
clonedFrame
;
}
const
sample
=
globalSamples
[
frame_n
];
if
(
sample
!=
null
)
{
const
duration
=
(
sample
.
duration
*
1
_000_000
)
/
sample
.
timescale
;
imageFrames
.
push
({
bitmap
:
inputFrame
,
timestamp
:
inputFrame
.
timestamp
,
duration
,
});
// Sort frames in order of timestamp. This is needed because Safari
// can return decoded frames out of order.
imageFrames
.
sort
((
a
,
b
)
=>
(
a
.
timestamp
<
b
.
timestamp
?
1
:
-
1
));
// Update progress on first frame and then every 40th frame
if
(
onProgress
!=
null
&&
frame_n
%
100
===
0
)
{
onProgress
({
width
:
saveTrack
.
track_width
,
height
:
saveTrack
.
track_height
,
frames
:
imageFrames
,
numFrames
:
saveTrack
.
nb_samples
,
fps
:
(
saveTrack
.
nb_samples
/
saveTrack
.
duration
)
*
saveTrack
.
timescale
,
});
}
}
frame_n
++
;
if
(
saveTrack
.
nb_samples
===
frame_n
)
{
// Sort frames in order of timestamp. This is needed because Safari
// can return decoded frames out of order.
imageFrames
.
sort
((
a
,
b
)
=>
(
a
.
timestamp
>
b
.
timestamp
?
1
:
-
1
));
resolve
({
width
:
saveTrack
.
track_width
,
height
:
saveTrack
.
track_height
,
frames
:
imageFrames
,
numFrames
:
saveTrack
.
nb_samples
,
fps
:
(
saveTrack
.
nb_samples
/
saveTrack
.
duration
)
*
saveTrack
.
timescale
,
});
}
},
error
(
error
)
{
reject
(
error
);
},
});
let
description
;
const
trak
=
mp4File
.
getTrackById
(
track
.
id
);
const
entries
=
trak
?.
mdia
?.
minf
?.
stbl
?.
stsd
?.
entries
;
if
(
entries
==
null
)
{
return
;
}
for
(
const
entry
of
entries
)
{
if
(
entry
.
avcC
||
entry
.
hvcC
)
{
const
stream
=
new
DataStream
(
undefined
,
0
,
DataStream
.
BIG_ENDIAN
);
if
(
entry
.
avcC
)
{
entry
.
avcC
.
write
(
stream
);
}
else
if
(
entry
.
hvcC
)
{
entry
.
hvcC
.
write
(
stream
);
}
description
=
new
Uint8Array
(
stream
.
buffer
,
8
);
// Remove the box header.
break
;
}
}
const
configuration
:
VideoDecoderConfig
=
{
codec
:
track
.
codec
,
codedWidth
:
track
.
track_width
,
codedHeight
:
track
.
track_height
,
description
,
};
const
supportedConfig
=
await
VideoDecoder
.
isConfigSupported
(
configuration
);
if
(
supportedConfig
.
supported
==
true
)
{
decoder
.
configure
(
configuration
);
mp4File
.
setExtractionOptions
(
track
.
id
,
null
,
{
nbSamples
:
Infinity
,
});
mp4File
.
start
();
}
else
{
reject
(
new
Error
(
`Decoder config faile: config
${
JSON
.
stringify
(
supportedConfig
.
config
,
)}
is not supported`
,
),
);
return
;
}
};
mp4File
.
onSamples
=
async
(
_id
:
number
,
_user
:
unknown
,
samples
:
MP4Sample
[],
)
=>
{
for
(
const
sample
of
samples
)
{
globalSamples
.
push
(
sample
);
decoder
.
decode
(
new
EncodedVideoChunk
({
type
:
sample
.
is_sync
?
'
key
'
:
'
delta
'
,
timestamp
:
(
sample
.
cts
*
1
_000_000
)
/
sample
.
timescale
,
duration
:
(
sample
.
duration
*
1
_000_000
)
/
sample
.
timescale
,
data
:
sample
.
data
,
}),
);
}
await
decoder
.
flush
();
decoder
.
close
();
};
onReady
(
mp4File
);
});
}
export
function
decode
(
file
:
File
,
onProgress
:
(
decodedVideo
:
DecodedVideo
)
=>
void
,
):
Promise
<
DecodedVideo
>
{
return
decodeInternal
(
file
.
name
,
async
(
mp4File
:
MP4File
)
=>
{
const
reader
=
new
FileReader
();
reader
.
onload
=
function
()
{
const
result
=
this
.
result
as
MP4ArrayBuffer
;
if
(
result
!=
null
)
{
result
.
fileStart
=
0
;
mp4File
.
appendBuffer
(
result
);
}
mp4File
.
flush
();
};
reader
.
readAsArrayBuffer
(
file
);
},
onProgress
,
);
}
export
function
decodeStream
(
fileStream
:
FileStream
,
onProgress
:
(
decodedVideo
:
DecodedVideo
)
=>
void
,
):
Promise
<
DecodedVideo
>
{
return
decodeInternal
(
'
stream
'
,
async
(
mp4File
:
MP4File
)
=>
{
let
part
=
await
fileStream
.
next
();
while
(
part
.
done
===
false
)
{
const
result
=
part
.
value
.
data
.
buffer
as
MP4ArrayBuffer
;
if
(
result
!=
null
)
{
result
.
fileStart
=
part
.
value
.
range
.
start
;
mp4File
.
appendBuffer
(
result
);
}
mp4File
.
flush
();
part
=
await
fileStream
.
next
();
}
},
onProgress
,
);
}
demo/frontend/src/common/codecs/VideoEncoder.ts
0 → 100644
View file @
17d316f3
/**
* 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
{
ImageFrame
}
from
'
@/common/codecs/VideoDecoder
'
;
import
{
MP4ArrayBuffer
,
createFile
}
from
'
mp4box
'
;
// The selection of timescale and seconds/key-frame value are
// explained in the following docs: https://github.com/vjeux/mp4-h264-re-encode
const
TIMESCALE
=
90000
;
const
SECONDS_PER_KEY_FRAME
=
2
;
export
function
encode
(
width
:
number
,
height
:
number
,
numFrames
:
number
,
framesGenerator
:
AsyncGenerator
<
ImageFrame
,
unknown
>
,
progressCallback
?:
(
progress
:
number
)
=>
void
,
):
Promise
<
MP4ArrayBuffer
>
{
return
new
Promise
((
resolve
,
reject
)
=>
{
let
encodedFrameIndex
=
0
;
let
nextKeyFrameTimestamp
=
0
;
let
trackID
:
number
|
null
=
null
;
const
durations
:
number
[]
=
[];
const
outputFile
=
createFile
();
const
encoder
=
new
VideoEncoder
({
output
(
chunk
,
metaData
)
{
const
uint8
=
new
Uint8Array
(
chunk
.
byteLength
);
chunk
.
copyTo
(
uint8
);
const
description
=
metaData
?.
decoderConfig
?.
description
;
if
(
trackID
===
null
)
{
trackID
=
outputFile
.
addTrack
({
width
:
width
,
height
:
height
,
timescale
:
TIMESCALE
,
avcDecoderConfigRecord
:
description
,
});
}
const
shiftedDuration
=
durations
.
shift
();
if
(
shiftedDuration
!=
null
)
{
outputFile
.
addSample
(
trackID
,
uint8
,
{
duration
:
getScaledDuration
(
shiftedDuration
),
is_sync
:
chunk
.
type
===
'
key
'
,
});
encodedFrameIndex
++
;
progressCallback
?.(
encodedFrameIndex
/
numFrames
);
}
if
(
encodedFrameIndex
===
numFrames
)
{
resolve
(
outputFile
.
getBuffer
());
}
},
error
(
error
)
{
reject
(
error
);
return
;
},
});
const
setConfigurationAndEncodeFrames
=
async
()
=>
{
// The codec value was taken from the following implementation and seems
// reasonable for our use case for now:
// https://github.com/vjeux/mp4-h264-re-encode/blob/main/mp4box.html#L103
// Additional details about codecs can be found here:
// - https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter
// - https://www.w3.org/TR/webcodecs-codec-registry/#video-codec-registry
//
// The following setting is a good compromise between output video file
// size and quality. The latencyMode "realtime" is needed for Safari,
// which otherwise will produce 20x larger files when in quality
// latencyMode. Chrome does a really good job with file size even when
// latencyMode is set to quality.
const
configuration
:
VideoEncoderConfig
=
{
codec
:
'
avc1.4d0034
'
,
width
:
roundToNearestEven
(
width
),
height
:
roundToNearestEven
(
height
),
bitrate
:
14
_000_000
,
alpha
:
'
discard
'
,
bitrateMode
:
'
variable
'
,
latencyMode
:
'
realtime
'
,
};
const
supportedConfig
=
await
VideoEncoder
.
isConfigSupported
(
configuration
);
if
(
supportedConfig
.
supported
===
true
)
{
encoder
.
configure
(
configuration
);
}
else
{
throw
new
Error
(
`Unsupported video encoder config
${
JSON
.
stringify
(
supportedConfig
)}
`
,
);
}
for
await
(
const
frame
of
framesGenerator
)
{
const
{
bitmap
,
duration
,
timestamp
}
=
frame
;
durations
.
push
(
duration
);
let
keyFrame
=
false
;
if
(
timestamp
>=
nextKeyFrameTimestamp
)
{
await
encoder
.
flush
();
keyFrame
=
true
;
nextKeyFrameTimestamp
=
timestamp
+
SECONDS_PER_KEY_FRAME
*
1
e6
;
}
encoder
.
encode
(
bitmap
,
{
keyFrame
});
bitmap
.
close
();
}
await
encoder
.
flush
();
encoder
.
close
();
};
setConfigurationAndEncodeFrames
();
});
}
function
getScaledDuration
(
rawDuration
:
number
)
{
return
rawDuration
/
(
1
_000_000
/
TIMESCALE
);
}
function
roundToNearestEven
(
dim
:
number
)
{
const
rounded
=
Math
.
round
(
dim
);
if
(
rounded
%
2
===
0
)
{
return
rounded
;
}
else
{
return
rounded
+
(
rounded
>
dim
?
-
1
:
1
);
}
}
demo/frontend/src/common/codecs/WebCodecUtils.ts
0 → 100644
View file @
17d316f3
/**
* 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.
*/
// https://github.com/w3c/webcodecs/issues/88
// https://issues.chromium.org/issues/40725065
// https://webcodecs-blogpost-demo.glitch.me/
export
async
function
cloneFrame
(
frame
:
VideoFrame
):
Promise
<
VideoFrame
>
{
const
{
codedHeight
,
codedWidth
,
colorSpace
,
displayHeight
,
displayWidth
,
format
,
timestamp
,
}
=
frame
;
const
rect
=
{
x
:
0
,
y
:
0
,
width
:
codedWidth
,
height
:
codedHeight
};
const
data
=
new
ArrayBuffer
(
frame
.
allocationSize
({
rect
}));
try
{
await
frame
.
copyTo
(
data
,
{
rect
});
}
catch
(
error
)
{
// The VideoFrame#copyTo on x64 builds on macOS fails. The workaround here
// is to clone the frame.
// https://stackoverflow.com/questions/77898766/inconsistent-behavior-of-webcodecs-copyto-method-across-different-browsers-an
return
frame
.
clone
();
}
return
new
VideoFrame
(
data
,
{
codedHeight
,
codedWidth
,
colorSpace
,
displayHeight
,
displayWidth
,
duration
:
frame
.
duration
??
undefined
,
format
:
format
!
,
timestamp
,
visibleRect
:
frame
.
visibleRect
??
undefined
,
});
}
demo/frontend/src/common/components/MobileFirstClickBanner.tsx
0 → 100644
View file @
17d316f3
/**
* 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
ChangeVideoModal
from
'
@/common/components/gallery/ChangeVideoModal
'
;
import
{
DEMO_SHORT_NAME
}
from
'
@/demo/DemoConfig
'
;
import
{
spacing
}
from
'
@/theme/tokens.stylex
'
;
import
{
ImageCopy
}
from
'
@carbon/icons-react
'
;
import
stylex
from
'
@stylexjs/stylex
'
;
import
{
Button
}
from
'
react-daisyui
'
;
const
styles
=
stylex
.
create
({
container
:
{
position
:
'
relative
'
,
backgroundColor
:
'
#000
'
,
padding
:
spacing
[
5
],
paddingVertical
:
spacing
[
6
],
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
spacing
[
4
],
},
});
export
default
function
MobileFirstClickBanner
()
{
return
(
<
div
{
...
stylex
.
props
(
styles
.
container
)
}
>
<
div
className
=
"flex text-white text-lg"
>
Click an object in the video to start
</
div
>
<
div
className
=
"text-sm text-[#A7B3BF]"
>
<
p
>
You
'
ll be able to use
{
DEMO_SHORT_NAME
}
to make fun edits to any
video by tracking objects and applying visual effects. To start, click
any object in the video.
</
p
>
</
div
>
<
div
className
=
"flex items-center"
>
<
ChangeVideoModal
videoGalleryModalTrigger
=
{
MobileVideoGalleryModalTrigger
}
showUploadInGallery
=
{
true
}
/>
</
div
>
</
div
>
);
}
type
MobileVideoGalleryModalTriggerProps
=
{
onClick
:
()
=>
void
;
};
function
MobileVideoGalleryModalTrigger
({
onClick
,
}:
MobileVideoGalleryModalTriggerProps
)
{
return
(
<
Button
color
=
"ghost"
startIcon
=
{
<
ImageCopy
size
=
{
20
}
/>
}
onClick
=
{
onClick
}
className
=
"text-white p-0"
>
Change video
</
Button
>
);
}
demo/frontend/src/common/components/Tooltip.tsx
0 → 100644
View file @
17d316f3
/**
* 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
{
PropsWithChildren
}
from
'
react
'
;
type
Props
=
PropsWithChildren
<
{
className
?:
string
;
message
:
string
;
position
?:
'
left
'
|
'
top
'
|
'
right
'
|
'
bottom
'
;
}
>
;
/**
* This is a custom Tooltip component because React Daisy UI does not have an
* option to *only* show tooltip on large devices.
*/
export
default
function
Tooltip
({
children
,
className
=
''
,
message
,
position
=
'
top
'
,
}:
Props
)
{
return
(
<
div
className
=
{
`lg:tooltip tooltip-
${
position
}
${
className
}
`
}
data
-
tip
=
{
message
}
>
{
children
}
</
div
>
);
}
demo/frontend/src/common/components/annotations/AddObjectButton.tsx
0 → 100644
View file @
17d316f3
/**
* 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
useMessagesSnackbar
from
'
@/common/components/snackbar/useDemoMessagesSnackbar
'
;
import
useVideo
from
'
@/common/components/video/editor/useVideo
'
;
import
{
activeTrackletObjectIdAtom
,
labelTypeAtom
}
from
'
@/demo/atoms
'
;
import
{
Add
}
from
'
@carbon/icons-react
'
;
import
{
useSetAtom
}
from
'
jotai
'
;
export
default
function
AddObjectButton
()
{
const
video
=
useVideo
();
const
setActiveTrackletId
=
useSetAtom
(
activeTrackletObjectIdAtom
);
const
setLabelType
=
useSetAtom
(
labelTypeAtom
);
const
{
enqueueMessage
}
=
useMessagesSnackbar
();
async
function
addObject
()
{
enqueueMessage
(
'
addObjectClick
'
);
const
tracklet
=
await
video
?.
createTracklet
();
if
(
tracklet
!=
null
)
{
setActiveTrackletId
(
tracklet
.
id
);
setLabelType
(
'
positive
'
);
}
}
return
(
<
div
onClick
=
{
addObject
}
className
=
"group flex justify-start mx-4 px-4 bg-transparent text-white !rounded-xl border-none cursor-pointer"
>
<
div
className
=
"flex gap-6 items-center"
>
<
div
className
=
" group-hover:bg-graydark-700 border border-white relative h-12 w-12 md:w-20 md:h-20 shrink-0 rounded-lg flex items-center justify-center"
>
<
Add
size
=
{
36
}
className
=
"group-hover:text-white text-gray-300"
/>
</
div
>
<
div
className
=
"font-medium text-base"
>
Add another object
</
div
>
</
div
>
</
div
>
);
}
demo/frontend/src/common/components/annotations/ClearAllPointsInVideoButton.tsx
0 → 100644
View file @
17d316f3
/**
* 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
useRestartSession
from
'
@/common/components/session/useRestartSession
'
;
import
useMessagesSnackbar
from
'
@/common/components/snackbar/useDemoMessagesSnackbar
'
;
import
useVideo
from
'
@/common/components/video/editor/useVideo
'
;
import
{
isPlayingAtom
,
isStreamingAtom
,
labelTypeAtom
}
from
'
@/demo/atoms
'
;
import
{
Reset
}
from
'
@carbon/icons-react
'
;
import
stylex
from
'
@stylexjs/stylex
'
;
import
{
useAtomValue
,
useSetAtom
}
from
'
jotai
'
;
import
{
useState
}
from
'
react
'
;
import
{
Button
,
Loading
}
from
'
react-daisyui
'
;
const
styles
=
stylex
.
create
({
container
:
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
},
});
type
Props
=
{
onRestart
:
()
=>
void
;
};
export
default
function
ClearAllPointsInVideoButton
({
onRestart
}:
Props
)
{
const
[
isLoading
,
setIsLoading
]
=
useState
<
boolean
>
(
false
);
const
isPlaying
=
useAtomValue
(
isPlayingAtom
);
const
isStreaming
=
useAtomValue
(
isStreamingAtom
);
const
setLabelType
=
useSetAtom
(
labelTypeAtom
);
const
{
clearMessage
}
=
useMessagesSnackbar
();
const
{
restartSession
}
=
useRestartSession
();
const
video
=
useVideo
();
async
function
handleRestart
()
{
if
(
video
===
null
)
{
return
;
}
setIsLoading
(
true
);
if
(
isPlaying
)
{
video
.
pause
();
}
if
(
isStreaming
)
{
await
video
.
abortStreamMasks
();
}
const
isSuccessful
=
await
video
.
clearPointsInVideo
();
if
(
!
isSuccessful
)
{
await
restartSession
();
}
video
.
frame
=
0
;
setLabelType
(
'
positive
'
);
onRestart
();
clearMessage
();
setIsLoading
(
false
);
}
return
(
<
div
{
...
stylex
.
props
(
styles
.
container
)
}
>
<
Button
color
=
"ghost"
onClick
=
{
handleRestart
}
className
=
"!px-4 !rounded-full font-medium text-white hover:bg-black"
startIcon
=
{
isLoading
?
<
Loading
size
=
"sm"
/>
:
<
Reset
size
=
{
20
}
/>
}
>
Start over
</
Button
>
</
div
>
);
}
demo/frontend/src/common/components/annotations/CloseSessionButton.tsx
0 → 100644
View file @
17d316f3
/**
* 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
PrimaryCTAButton
from
'
@/common/components/button/PrimaryCTAButton
'
;
import
useVideo
from
'
@/common/components/video/editor/useVideo
'
;
import
{
ChevronRight
}
from
'
@carbon/icons-react
'
;
type
Props
=
{
onSessionClose
:
()
=>
void
;
};
export
default
function
CloseSessionButton
({
onSessionClose
}:
Props
)
{
const
video
=
useVideo
();
function
handleCloseSession
()
{
video
?.
closeSession
();
video
?.
logAnnotations
();
onSessionClose
();
}
return
(
<
PrimaryCTAButton
onClick
=
{
handleCloseSession
}
endIcon
=
{
<
ChevronRight
/>
}
>
Good to go
</
PrimaryCTAButton
>
);
}
demo/frontend/src/common/components/annotations/FirstClickView.tsx
0 → 100644
View file @
17d316f3
/**
* 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
ChangeVideo
from
'
@/common/components/gallery/ChangeVideoModal
'
;
import
useMessagesSnackbar
from
'
@/common/components/snackbar/useDemoMessagesSnackbar
'
;
import
{
DEMO_SHORT_NAME
}
from
'
@/demo/DemoConfig
'
;
import
{
useEffect
,
useRef
}
from
'
react
'
;
export
default
function
FirstClickView
()
{
const
isFirstClickMessageShown
=
useRef
(
false
);
const
{
enqueueMessage
}
=
useMessagesSnackbar
();
useEffect
(()
=>
{
if
(
!
isFirstClickMessageShown
.
current
)
{
isFirstClickMessageShown
.
current
=
true
;
enqueueMessage
(
'
firstClick
'
);
}
},
[
enqueueMessage
]);
return
(
<
div
className
=
"w-full h-full flex flex-col p-8"
>
<
div
className
=
"grow flex flex-col gap-6"
>
<
h2
className
=
"text-2xl"
>
Click an object in the video to start
</
h2
>
<
p
className
=
"!text-gray-60"
>
You
'
ll be able to use
{
DEMO_SHORT_NAME
}
to make fun edits to any
video by tracking objects and applying visual effects.
</
p
>
<
p
className
=
"!text-gray-60"
>
To start, click any object in the video.
</
p
>
</
div
>
<
div
className
=
"flex items-center"
>
<
ChangeVideo
/>
</
div
>
</
div
>
);
}
demo/frontend/src/common/components/annotations/LimitNotice.tsx
0 → 100644
View file @
17d316f3
/**
* 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
{
InformationFilled
}
from
'
@carbon/icons-react
'
;
export
default
function
LimitNotice
()
{
return
(
<
div
className
=
"mt-6 gap-3 mx-6 flex items-center text-gray-400"
>
<
div
>
<
InformationFilled
size
=
{
32
}
/>
</
div
>
<
div
className
=
"text-sm leading-snug"
>
In this demo, you can track up to 3 objects, even though the SAM 2 model
does not have a limit.
</
div
>
</
div
>
);
}
demo/frontend/src/common/components/annotations/MobileObjectsList.tsx
0 → 100644
View file @
17d316f3
/**
* 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
ClearAllPointsInVideoButton
from
'
@/common/components/annotations/ClearAllPointsInVideoButton
'
;
import
ObjectThumbnail
from
'
@/common/components/annotations/ObjectThumbnail
'
;
import
{
OBJECT_TOOLBAR_INDEX
}
from
'
@/common/components/toolbar/ToolbarConfig
'
;
import
{
BaseTracklet
}
from
'
@/common/tracker/Tracker
'
;
import
{
activeTrackletObjectIdAtom
,
trackletObjectsAtom
}
from
'
@/demo/atoms
'
;
import
{
spacing
}
from
'
@/theme/tokens.stylex
'
;
import
stylex
from
'
@stylexjs/stylex
'
;
import
{
useAtomValue
,
useSetAtom
}
from
'
jotai
'
;
const
styles
=
stylex
.
create
({
container
:
{
display
:
'
flex
'
,
padding
:
spacing
[
5
],
borderTop
:
'
1px solid #DEE3E9
'
,
},
trackletsContainer
:
{
flexGrow
:
1
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
spacing
[
5
],
},
});
type
Props
=
{
showActiveObject
:
()
=>
void
;
onTabChange
:
(
newIndex
:
number
)
=>
void
;
};
export
default
function
MobileObjectsList
({
showActiveObject
,
onTabChange
,
}:
Props
)
{
const
tracklets
=
useAtomValue
(
trackletObjectsAtom
);
const
setActiveTrackletId
=
useSetAtom
(
activeTrackletObjectIdAtom
);
function
handleSelectObject
(
tracklet
:
BaseTracklet
)
{
setActiveTrackletId
(
tracklet
.
id
);
showActiveObject
();
}
return
(
<
div
{
...
stylex
.
props
(
styles
.
container
)
}
>
<
div
{
...
stylex
.
props
(
styles
.
trackletsContainer
)
}
>
{
tracklets
.
map
(
tracklet
=>
{
const
{
id
,
color
,
thumbnail
}
=
tracklet
;
return
(
<
ObjectThumbnail
key
=
{
id
}
color
=
{
color
}
thumbnail
=
{
thumbnail
}
onClick
=
{
()
=>
{
handleSelectObject
(
tracklet
);
}
}
/>
);
})
}
</
div
>
<
ClearAllPointsInVideoButton
onRestart
=
{
()
=>
onTabChange
(
OBJECT_TOOLBAR_INDEX
)
}
/>
</
div
>
);
}
demo/frontend/src/common/components/annotations/MobileObjectsToolbar.tsx
0 → 100644
View file @
17d316f3
/**
* 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
MobileObjectsToolbarHeader
from
'
@/common/components/annotations/MobileObjectsToolbarHeader
'
;
import
ObjectsToolbarBottomActions
from
'
@/common/components/annotations/ObjectsToolbarBottomActions
'
;
import
{
getObjectLabel
}
from
'
@/common/components/annotations/ObjectUtils
'
;
import
ToolbarObject
from
'
@/common/components/annotations/ToolbarObject
'
;
import
MobileFirstClickBanner
from
'
@/common/components/MobileFirstClickBanner
'
;
import
{
activeTrackletObjectAtom
,
isFirstClickMadeAtom
}
from
'
@/demo/atoms
'
;
import
{
useAtomValue
}
from
'
jotai
'
;
type
Props
=
{
onTabChange
:
(
newIndex
:
number
)
=>
void
;
};
export
default
function
MobileObjectsToolbar
({
onTabChange
}:
Props
)
{
const
activeTracklet
=
useAtomValue
(
activeTrackletObjectAtom
);
const
isFirstClickMade
=
useAtomValue
(
isFirstClickMadeAtom
);
if
(
!
isFirstClickMade
)
{
return
<
MobileFirstClickBanner
/>;
}
return
(
<
div
className
=
"w-full"
>
<
MobileObjectsToolbarHeader
/>
{
activeTracklet
!=
null
&&
(
<
ToolbarObject
label
=
{
getObjectLabel
(
activeTracklet
)
}
tracklet
=
{
activeTracklet
}
isActive
=
{
true
}
isMobile
=
{
true
}
/>
)
}
<
ObjectsToolbarBottomActions
onTabChange
=
{
onTabChange
}
/>
</
div
>
);
}
demo/frontend/src/common/components/annotations/MobileObjectsToolbarHeader.tsx
0 → 100644
View file @
17d316f3
/**
* 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
ToolbarProgressChip
from
'
@/common/components/toolbar/ToolbarProgressChip
'
;
import
{
isStreamingAtom
,
streamingStateAtom
}
from
'
@/demo/atoms
'
;
import
{
useAtomValue
}
from
'
jotai
'
;
export
default
function
MobileObjectsToolbarHeader
()
{
const
isStreaming
=
useAtomValue
(
isStreamingAtom
);
const
streamingState
=
useAtomValue
(
streamingStateAtom
);
return
(
<
div
className
=
"w-full flex gap-4 items-center px-5 py-5"
>
<
div
className
=
"grow text-sm text-white"
>
<
ToolbarProgressChip
/>
{
streamingState
===
'
full
'
?
'
Review your selected objects across the video, and continue to edit if needed. Once everything looks good, press “Next” to continue.
'
:
isStreaming
?
'
Watch the video closely for any places where your objects aren’t tracked correctly. You can also stop tracking to make additional edits.
'
:
'
Edit your object selection with a few more clicks if needed. Press “Track objects” to track your objects throughout the video.
'
}
</
div
>
</
div
>
);
}
Prev
1
…
19
20
21
22
23
24
25
26
27
…
48
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment