Compare commits

..

23 Commits

Author SHA1 Message Date
ElementalAlchemist 4298937535 Fix losing milliseconds sometimes 3 months ago
ElementalAlchemist 5143d15f68 Fix time ago conversion occurring in the wrong direction 3 months ago
ElementalAlchemist 0fd6a09b1c Store player settings 3 months ago
ElementalAlchemist 2095880d10 Switch to more native video controls 3 months ago
ElementalAlchemist bb85eb494d Remove the default video controls 3 months ago
ElementalAlchemist aa649ac4ac Implement basic video controls
These will be expanded or replaced with vidstack's controls in the future, but this at least gives us most of the basic requirements.
3 months ago
ElementalAlchemist aeebf0b7ad Bound max video player size 3 months ago
ElementalAlchemist 9a8be2f875 Make the video load bar work properly 3 months ago
ElementalAlchemist 0aed412ccb Add vidstack video player 3 months ago
ElementalAlchemist 89ff17564e Basic loading of videos 3 months ago
ElementalAlchemist 6608555a8b Add cancel/reset buttons for thumbnail edit 3 months ago
ElementalAlchemist 87e8333d05 Add video load time fields to restreamer 3 months ago
ElementalAlchemist 4517ec1e68 Add errors and keyboard shortcuts to restreamer 3 months ago
ElementalAlchemist 5ab145b031 Add components for keyboard shortcuts 3 months ago
ElementalAlchemist 2bb8ff6245 Add Hls.js as a dependency 3 months ago
ElementalAlchemist 7de2f807b1 Move luxon dependency to npm 3 months ago
ElementalAlchemist c3448542c4 Add template form 3 months ago
ElementalAlchemist 6b381428ed Enable viewing and editing thumbnail templates 3 months ago
ElementalAlchemist ae808eedde Fix pressing "enter" in time converter reloading the page 3 months ago
ElementalAlchemist 369b70bb19 Use UTC as the default time zone when converting if no other time zone is specified 3 months ago
ElementalAlchemist 1c842d9d16 Add the time converter utility 3 months ago
ElementalAlchemist ea438c73db Update Dockerfile to build Thrimbletrimmer when building images 3 months ago
ElementalAlchemist 05a53924f6 Initial structure and clock
This is the start of replacing the old Thrimbletrimmer with a new SolidJS-based Thrimbletrimmer. It includes an implementation of the clock page for a basic implementation of signal-based page.
3 months ago

@ -314,14 +314,14 @@ def recognize_time_of_day(frame):
'day': (82, 218, 217),
'dusk': (217, 150, 181),
'night': (0, 0, 0),
'dawn': (36, 38, 117),
'dawn': (56, 53, 125), # estimated from previous years
}
dash_colours = {
'score': (181, 181, 150),
'day': (146, 0, 1),
'dusk': (115, 0, 0),
'night': (41, 0, 0),
'dawn': (78, 0, 0),
'dawn': (118, 0, 0), # estimated from previous years
}
threshold = 20 # use stronger constraint once we have dusk, night and dawn footage
sky_pixel = frame.getpixel((1614, 192))
@ -344,10 +344,10 @@ def recognize_time_of_day(frame):
return best, distance
def extract_segment(prototypes, segment, time):
def extract_segment(prototypes, segment):
ODO_SCORE_THRESHOLD = 0.01
CLOCK_SCORE_THRESHOLD = 0.01
frame_data = b"".join(extract_frame([segment], time))
frame_data = b"".join(extract_frame([segment], segment.start))
frame = Image.open(BytesIO(frame_data))
odometer, score, _ = recognize_odometer(prototypes, frame)
if score < ODO_SCORE_THRESHOLD:

@ -26,7 +26,7 @@ def do_extract_segment(*segment_paths, prototypes_path="./prototypes"):
prototypes = load_prototypes(prototypes_path)
for segment_path in segment_paths:
segment_info = parse_segment_path(segment_path)
odometer, clock, tod = extract_segment(prototypes, segment_info, segment_info.start)
odometer, clock, tod = extract_segment(prototypes, segment_info)
print(f"{segment_path} {odometer} {clock} {tod}")
@ -76,7 +76,7 @@ def compare_segments(dbconnect, base_dir='.', prototypes_path="./prototypes", si
for old_odometer, (segment, old_clock, old_tod) in selected:
path = os.path.join(base_dir, segment)
segment_info = parse_segment_path(path)
odometer, clock, tod = extract_segment(prototypes, segment_info, segment_info.start)
odometer, clock, tod = extract_segment(prototypes, segment_info)
results.append((segment, {
"odo": (old_odometer, odometer),
"clock": (old_clock, clock),
@ -118,13 +118,8 @@ def analyze_segment(db_manager, prototypes, segment_path, check_segment_name=Non
if check_segment_name is not None:
assert segment_name == check_segment_name
# A timestamp fully at the end doesn't get us a valid frame.
# But we want to be as late as possible to minimize latency.
# We attempt to do a fixed time before the end, or use the start if too short.
timestamp = max(segment_info.start, segment_info.end - datetime.timedelta(seconds=0.1))
try:
odometer, clock, tod = extract_segment(prototypes, segment_info, timestamp)
odometer, clock, tod = extract_segment(prototypes, segment_info)
except Exception:
logging.warning(f"Failed to extract segment {segment_path!r}", exc_info=True)
odometer = None
@ -148,7 +143,7 @@ def analyze_segment(db_manager, prototypes, segment_path, check_segment_name=Non
timeofday = %(timeofday)s
""",
channel=segment_info.channel,
timestamp=timestamp,
timestamp=segment_info.start,
segment=segment_name,
error=error,
odometer=odometer,
@ -205,7 +200,7 @@ def main(
prototypes_path="./prototypes",
concurrency=10,
):
CHECK_INTERVAL = 0.5
CHECK_INTERVAL = 2
stopping = gevent.event.Event()

@ -397,8 +397,8 @@ def cut(channel, quality):
type = request.args.get('type', 'fast')
if type == 'rough':
# NOTE: We intentionally ignore transitions for rough cuts, as these are mostly used
# when downloading source footage for later editing.
if has_transitions:
return "Cannot do rough cut with transitions", 400
return Response(rough_cut_segments(segment_ranges, ranges), mimetype='video/MP2T')
elif type == 'fast':
return Response(fast_cut_segments(segment_ranges, ranges, transitions), mimetype='video/MP2T')

@ -1,6 +1,4 @@
import { DateTime } from "luxon";
import { HLSProvider } from "vidstack";
import { Fragment } from "hls.js";
export enum TimeType {
UTC,
@ -101,29 +99,3 @@ export function timeAgoFromDateTime(dateTime: DateTime): string {
return `${negative}${hours}:${minutesString}:${secondsString}`;
}
export function dateTimeFromVideoPlayerTime(
videoProvider: HLSProvider,
videoTime: number,
): DateTime | null {
// We do a little bit of cheating our way into private data. The standard way of getting this involves
// handling events and capturing and saving a fragments array from it, which seems overly cumbersome.
const fragments = (videoProvider.instance as any).latencyController.levelDetails
.fragments as Fragment[];
let fragmentStartTime: number | undefined = undefined;
let fragmentStartISOTime: string | undefined = undefined;
for (const fragment of fragments) {
const fragmentEndTime = fragment.start + fragment.duration;
if (videoTime >= fragment.start && videoTime < fragmentEndTime) {
fragmentStartTime = fragment.start;
fragmentStartISOTime = fragment.rawProgramDateTime;
break;
}
}
if (fragmentStartISOTime === undefined) {
return null;
}
const wubloaderTime = DateTime.fromISO(fragmentStartISOTime);
const offset = videoTime - fragmentStartTime;
return wubloaderTime.plus({ seconds: offset });
}

@ -8,4 +8,4 @@
.streamTimeSettingLabel {
margin-right: 3px;
}
}

@ -1,12 +1,11 @@
// Used to make the controls appear below the video player
media-player {
flex-direction: column;
max-width: 100%;
max-height: 50vh;
}
// This level of specificity is required to override player CSS
#root > media-player > media-provider {
align-items: normal;
media-provider, media-captions, video {
max-width: 100%;
max-height: 50vh;
}
// Used to make the controls appear below the video player
@ -27,4 +26,4 @@ media-volume-slider {
.vds-slider-track-fill {
background-color: #f6f6f6;
}
}

@ -21,7 +21,6 @@ import {
} from "./convertTime";
import styles from "./video.module.scss";
import "./video.scss";
import { HLSProvider } from "vidstack";
import { MediaPlayerElement } from "vidstack/elements";
import "vidstack/icons";
@ -201,14 +200,12 @@ export const StreamTimeSettings: Component<StreamTimeSettingsProps> = (props) =>
export interface VideoPlayerProps {
src: Accessor<string>;
setPlayerTime: Setter<number>;
mediaPlayer: Accessor<MediaPlayerElement>;
setMediaPlayer: Setter<MediaPlayerElement>;
}
export const VideoPlayer: Component<VideoPlayerProps> = (props) => {
let [mediaPlayer, setMediaPlayer] = createSignal<MediaPlayerElement>();
createEffect(() => {
const player = props.mediaPlayer();
const player = mediaPlayer();
const srcURL = props.src();
player.src = srcURL;
});
@ -217,10 +214,9 @@ export const VideoPlayer: Component<VideoPlayerProps> = (props) => {
let [duration, setDuration] = createSignal(0);
onMount(() => {
const player = props.mediaPlayer();
const player = mediaPlayer();
player.subscribe(({ currentTime, duration }) => {
setPlayerTime(currentTime);
props.setPlayerTime(currentTime);
setDuration(duration);
});
player.streamType = "on-demand";
@ -247,13 +243,14 @@ export const VideoPlayer: Component<VideoPlayerProps> = (props) => {
return (
<media-player
src={props.src()}
ref={props.setMediaPlayer}
ref={setMediaPlayer}
preload="auto"
controlsDelay={0}
storage="thrimbletrimmer"
>
<media-provider
<media-provider
onClick={(event) => {
const player = props.mediaPlayer();
const player = mediaPlayer();
if (player.paused) {
player.play(event);
} else {
@ -262,7 +259,7 @@ export const VideoPlayer: Component<VideoPlayerProps> = (props) => {
}}
/>
<media-captions class="vds-captions" />
<media-controls class="vds-controls" hideDelay={0}>
<media-controls class="vds-controls">
<media-controls-group class="vds-controls-group">
<media-tooltip>
<media-tooltip-trigger>

@ -5,6 +5,7 @@ body {
background: #222;
color: #fff;
height: 100vh;
margin: 0;
}
@ -42,4 +43,4 @@ a,
.hidden {
display: none;
}
}

@ -19,7 +19,3 @@
top: 0;
right: 0;
}
.videoLinks a {
margin: 2px;
}

@ -10,14 +10,8 @@ import {
Suspense,
} from "solid-js";
import { DateTime } from "luxon";
import { HLSProvider } from "vidstack";
import { MediaPlayerElement } from "vidstack/elements";
import styles from "./Restreamer.module.scss";
import {
dateTimeFromVideoPlayerTime,
dateTimeFromWubloaderTime,
wubloaderTimeFromDateTime,
} from "../common/convertTime";
import { dateTimeFromWubloaderTime, wubloaderTimeFromDateTime } from "../common/convertTime";
import {
KeyboardShortcuts,
StreamTimeSettings,
@ -98,8 +92,6 @@ const RestreamerWithDefaults: Component<RestreamerDefaultProps> = (props) => {
streamStartTime: DateTime.utc().minus({ minutes: 10 }),
streamEndTime: null,
});
const [playerTime, setPlayerTime] = createSignal<number>(0);
const [mediaPlayer, setMediaPlayer] = createSignal<MediaPlayerElement>();
const videoURL = () => {
const streamInfo = streamVideoInfo();
@ -117,33 +109,6 @@ const RestreamerWithDefaults: Component<RestreamerDefaultProps> = (props) => {
return url;
};
const downloadVideoURL = () => {
const streamInfo = streamVideoInfo();
const startTime = wubloaderTimeFromDateTime(streamInfo.streamStartTime);
const params = new URLSearchParams({ type: "smart", start: encodeURIComponent(startTime) });
if (streamInfo.streamEndTime) {
const endTime = wubloaderTimeFromDateTime(streamInfo.streamEndTime);
params.append("end", endTime);
}
return `/cut/${streamInfo.streamName}/source.ts?${params}`;
};
const downloadFrameURL = () => {
const streamInfo = streamVideoInfo();
const player = mediaPlayer();
const videoTime = playerTime();
const provider = player.provider as HLSProvider;
if (!provider) {
return "";
}
const currentTime = dateTimeFromVideoPlayerTime(provider, videoTime);
if (currentTime === null) {
return "";
}
const wubloaderTime = wubloaderTimeFromDateTime(currentTime);
return `/frame/${streamInfo.streamName}/source.png?timestamp=${wubloaderTime}`;
};
return (
<>
<StreamTimeSettings
@ -154,16 +119,7 @@ const RestreamerWithDefaults: Component<RestreamerDefaultProps> = (props) => {
errorList={props.errorList}
setErrorList={props.setErrorList}
/>
<VideoPlayer
src={videoURL}
setPlayerTime={setPlayerTime}
mediaPlayer={mediaPlayer}
setMediaPlayer={setMediaPlayer}
/>
<div class={styles.videoLinks}>
<a href={downloadVideoURL()}>Download Video</a>
<a href={downloadFrameURL()}>Download Current Frame as Image</a>
</div>
<VideoPlayer src={videoURL} />
</>
);
};

@ -130,8 +130,8 @@ def post_schedule(client, send_client, start_time, schedule, stream, hour, no_me
shift_hour = hour - omega + 1
if hour == last:
shift = "rdporb"
shift_hour = ""
shift = "crab"
shift_hour = 8.88
def render_name(user_id, mention=True):
if no_mentions:

Loading…
Cancel
Save