Skip to content

Commit e72037d

Browse files
authored
Merge pull request #357 from Northeastern-Electric-Racing/Uploadable-Playbackable-Videos
Uploadable playbackable videos
2 parents ae08fab + f2ffc6e commit e72037d

13 files changed

Lines changed: 367 additions & 34 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
**/.env
66
scylla-server-typescript/src/prisma/mydatabase.db
77
scylla-server-typescript/src/prisma/mydatabase.db-journal
8+
scylla-server/files
89

910
# Xcode
1011
#

angular-client/src/api/urls.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ const getAllRuns = () => `${baseURL}/runs`;
2323
const getLatestRun = () => `${baseURL}/runs/latest`;
2424
const startNewRun = () => `${baseURL}/runs/new`;
2525

26+
/* Videos */
27+
const getAllVideos = () => `${baseURL}/videos`;
28+
const getVideo = (fileName: string) => `${getAllVideos()}/${encodeURIComponent(fileName)}`;
29+
const updateVideos = () => `${getAllVideos()}/update`;
30+
2631
export const urls = {
2732
getAllDatatypes,
2833

@@ -34,5 +39,9 @@ export const urls = {
3439
getAllRuns,
3540
getLatestRun,
3641
getRunById,
37-
startNewRun
42+
startNewRun,
43+
44+
getAllVideos,
45+
getVideo,
46+
updateVideos
3847
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { urls } from './urls';
2+
3+
/**
4+
* Fetches all videos from the server
5+
* @returns A promise containing the response from the server
6+
*/
7+
export const getAllVideos = (): Promise<Response> => {
8+
return fetch(urls.getAllVideos());
9+
};
10+
11+
/**
12+
* Requests Scylla to update its videos
13+
* @returns A promise containing the response from scylla
14+
*/
15+
export const updateVideos = (): Promise<Response> => {
16+
return fetch(urls.updateVideos(), { method: 'POST' });
17+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.background {
2+
position: relative;
3+
}
4+
5+
.top-right {
6+
position: absolute;
7+
top: 10px;
8+
right: 10px;
9+
display: flex;
10+
gap: 10px; /* Adds spacing between the elements */
11+
align-items: center; /* Aligns elements vertically */
12+
z-index: 10;
13+
}
14+
15+
.video-container {
16+
width: 100%;
17+
height: 90vh;
18+
}

angular-client/src/pages/camera-page/camera-page.component.html

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
@if (this.urlAvailable) {
2-
<video #remoteVideo controls autoplay muted style="width: 100%; height: 90vh"></video>
2+
<div class="background">
3+
<div class="top-right">
4+
<p-select
5+
[options]="videoUrls"
6+
[(ngModel)]="selectedVideoName"
7+
placeholder="Select a Video"
8+
(onChange)="onDropdownChange($event)"
9+
[style]="{ padding: '5px' }"
10+
/>
11+
12+
<argos-button label="Update Videos" [onClick]="onUpdateVideosPressed" class="top-right-button" />
13+
</div>
14+
15+
<div class="video-container">
16+
@if (this.liveStream) {
17+
<video #remoteVideo controls autoplay muted style="width: 100%; height: 90vh"></video>
18+
} @else {
19+
<video #playbackVideo controls autoplay muted style="width: 100%; height: 90vh">
20+
<source [src]="playbackVideoUrl" type="video/mp4" />
21+
Your browser does not support the video tag.
22+
</video>
23+
}
24+
</div>
25+
</div>
326
} @else {
427
<div style="display: flex; justify-content: center; align-items: center; height: 100%">
528
<typography content="Camera Not Reachable" variant="xx-large-title" additionalStyles="color: red" />

angular-client/src/pages/camera-page/camera-page.component.spec.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,95 @@
1-
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
1+
import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core';
2+
import { MessageService } from 'primeng/api';
3+
import { SelectChangeEvent } from 'primeng/select';
4+
import { urls } from 'src/api/urls';
5+
import { getAllVideos, updateVideos } from 'src/api/video.api';
6+
import APIService from 'src/services/api.service';
27
import MediaMTXWebRTCReader from 'src/utils/MediaMTXReader';
38
@Component({
49
selector: 'camera-page',
510
templateUrl: './camera-page.component.html',
611
styleUrl: './camera-page.component.css'
712
})
813
export class CameraPageComponent implements OnInit {
14+
private serverService = inject(APIService);
15+
private messageService = inject(MessageService);
16+
917
url = 'http://192.168.100.11:8889/frontcam/';
1018
urlAvailable = false;
19+
liveStream = true;
20+
21+
selectedVideoName: string = 'Live Stream';
22+
playbackVideoUrl?: string;
23+
videoUrls: string[] = ['Live Stream'];
24+
videoUrlsIsLoading: boolean = true;
1125

1226
@ViewChild('remoteVideo') remoteVideo?: ElementRef<HTMLVideoElement>;
27+
@ViewChild('playbackVideo', { static: false }) playbackVideoRef!: ElementRef<HTMLVideoElement>;
1328

1429
async ngOnInit(): Promise<void> {
1530
this.urlAvailable = await this.checkConnection();
16-
console.log(window.location.href);
1731
new MediaMTXWebRTCReader({
1832
url: new URL('whep', this.url),
1933
onError: console.log,
2034
onTrack: (e) => {
21-
console.log(e);
2235
if (this.remoteVideo) {
2336
[this.remoteVideo.nativeElement.srcObject] = e.streams;
2437
}
2538
}
2639
});
40+
41+
const videosQueryResponse = this.serverService.query<string[]>(() => getAllVideos(), { queryKey: ['videos'] });
42+
videosQueryResponse.error.subscribe((error) => {
43+
error && this.messageService.add({ severity: 'error', summary: 'Error', detail: error.message });
44+
});
45+
videosQueryResponse.isLoading.subscribe((isLoading) => {
46+
this.videoUrlsIsLoading = isLoading;
47+
});
48+
videosQueryResponse.data.subscribe((data) => {
49+
if (data) this.videoUrls = data.concat('Live Stream');
50+
});
2751
}
2852

2953
checkConnection = (): Promise<boolean> =>
3054
fetch(this.url, { method: 'HEAD' })
3155
.then(() => true)
3256
.catch(() => false);
57+
58+
onVideoSelected = (videoName: string) => {
59+
this.playbackVideoUrl = urls.getVideo(videoName);
60+
this.selectedVideoName = videoName;
61+
this.liveStream = false;
62+
// Wait for Angular to update the DOM
63+
setTimeout(() => {
64+
const videoEl = this.playbackVideoRef.nativeElement;
65+
videoEl.load(); // This reloads the new <source> inside the <video>
66+
videoEl.play();
67+
});
68+
};
69+
70+
onLiveStreamSelected = () => {
71+
this.selectedVideoName = 'Live Stream';
72+
this.playbackVideoUrl = undefined;
73+
this.liveStream = true;
74+
};
75+
76+
onDropdownChange = (event: SelectChangeEvent) => {
77+
if (event.value === 'Live Stream') {
78+
this.onLiveStreamSelected();
79+
} else {
80+
this.onVideoSelected(event.value);
81+
}
82+
};
83+
84+
onUpdateVideosPressed = () => {
85+
const updateVideoQueryResponse = this.serverService.query(() => updateVideos(), { invalidates: ['videos'] });
86+
this.messageService.add({
87+
severity: 'success',
88+
summary: 'Update Videos',
89+
detail: 'A request was made for scylla to update its videos, this may take a minute, please refresh in a moment'
90+
});
91+
updateVideoQueryResponse.error.subscribe((error) => {
92+
error && this.messageService.add({ severity: 'error', summary: 'Error', detail: error.message });
93+
});
94+
};
3395
}

scylla-server/src/controllers/file_insertion_controller.rs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@ use axum_macros::debug_handler;
66
use chrono::DateTime;
77
use protobuf::CodedInputStream;
88
use rangemap::RangeInclusiveMap;
9-
use tokio::sync::mpsc;
9+
use tokio::{fs, sync::mpsc};
1010
use tracing::{debug, info, trace, warn};
1111

1212
use crate::{
1313
error::ScyllaError, proto::playback_data, services::run_service, ClientData, PoolHandle,
1414
};
1515

16-
/// Inserts a file using http multipart
16+
use super::OutputDirectory;
17+
18+
/// Inserts a logger file using http multipart
1719
/// This file is parsed and clientdata values are extracted, the run ID of each variable is inferred, and then data is batch uploaded
1820
// super cool: adding this tag tells you what variable is misbehaving in cases of axum Send+Sync Handler fails
1921
#[debug_handler]
20-
pub async fn insert_file(
22+
pub async fn insert_logger_file(
2123
State(pool): State<PoolHandle>,
2224
Extension(batcher): Extension<mpsc::Sender<Vec<ClientData>>>,
2325
mut multipart: Multipart,
@@ -50,7 +52,7 @@ pub async fn insert_file(
5052
while let Ok(Some(field)) = multipart.next_field().await {
5153
// round up all of the protobuf segments as a giant list
5254
let Ok(data) = field.bytes().await else {
53-
warn!("Could not decode file insert, perhaps it was interrupted!");
55+
warn!("Could not decode logger file insert, perhaps it was interrupted!");
5456
continue;
5557
};
5658
let mut count_bad_run = 0usize;
@@ -91,9 +93,38 @@ pub async fn insert_file(
9193
count_bad_run
9294
);
9395
if let Err(err) = batcher.send(insertable_data).await {
94-
warn!("Error sending file insert data to batcher! {}", err);
96+
warn!("Error sending logger file insert data to batcher! {}", err);
9597
};
9698
}
97-
info!("Finished file insert request!");
99+
info!("Finished logger file insert request!");
98100
Ok("Successfully sent all to batcher!".to_string())
99101
}
102+
103+
/// Writes the files in the multipart to a file on the server named with the multipart file name
104+
pub async fn insert_file(
105+
Extension(output_directory): Extension<OutputDirectory>,
106+
mut multipart: Multipart,
107+
) -> Result<String, ScyllaError> {
108+
while let Ok(Some(field)) = multipart.next_field().await {
109+
let name = field.name().map(|s| s.to_string());
110+
111+
let Ok(data) = field.bytes().await else {
112+
warn!("Could not decode file insert");
113+
continue;
114+
};
115+
116+
let Some(name) = name else {
117+
warn!("Could not get name");
118+
continue;
119+
};
120+
121+
info!("Inserting file: {}", name);
122+
123+
fs::write(format!("{}/{}", output_directory.0, name), data)
124+
.await
125+
.map_err(|e| ScyllaError::FileError(format!("Failed to write file {}", e)))?;
126+
}
127+
128+
info!("Finished file insert request!");
129+
Ok("Successfully wrote data to files".to_string())
130+
}

scylla-server/src/controllers/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,10 @@ pub mod data_type_controller;
66
pub mod run_controller;
77

88
pub mod file_insertion_controller;
9+
pub mod video_streamer_controller;
10+
11+
#[derive(Clone)]
12+
pub struct VideoSuffix(pub String);
13+
14+
#[derive(Clone)]
15+
pub struct OutputDirectory(pub String);

0 commit comments

Comments
 (0)