Compare commits
4 Commits
5e00965baa
...
prod
| Author | SHA1 | Date | |
|---|---|---|---|
| d6c703457c | |||
| 0b2c875889 | |||
| 7db25194bc | |||
| 3d57007074 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -40,7 +40,8 @@ build/
|
|||||||
|
|
||||||
### Generated Files ###
|
### Generated Files ###
|
||||||
/videos
|
/videos
|
||||||
outputs/
|
/outputs
|
||||||
|
frontend/.flowbite-react
|
||||||
|
|
||||||
generated
|
generated
|
||||||
.mvn
|
.mvn
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Eye, EyeOff, Copy, Check } from "lucide-react";
|
|||||||
import Box from "../components/Box.tsx";
|
import Box from "../components/Box.tsx";
|
||||||
import { getUser } from "../utils/endpoints.ts";
|
import { getUser } from "../utils/endpoints.ts";
|
||||||
import type { User } from "../utils/types.ts";
|
import type { User } from "../utils/types.ts";
|
||||||
|
import { formatLocalDate } from "../utils/utils.ts";
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
@@ -44,7 +45,7 @@ const Profile = () => {
|
|||||||
<p className="text-sm text-gray-500">@{user.username}</p>
|
<p className="text-sm text-gray-500">@{user.username}</p>
|
||||||
<p className="text-sm text-gray-500">{user.email}</p>
|
<p className="text-sm text-gray-500">{user.email}</p>
|
||||||
<p className="text-xs text-gray-400 mt-1">
|
<p className="text-xs text-gray-400 mt-1">
|
||||||
Member since {new Date(user.createdAt).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" })}
|
Member since {formatLocalDate(user.createdAt)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -7,12 +7,20 @@ function formatTime(seconds: number): string {
|
|||||||
return `${m}:${pad(s)}`;
|
return `${m}:${pad(s)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parses a UTC date string, appending Z if no timezone offset is present so
|
||||||
|
// the browser always interprets the value as UTC rather than local time.
|
||||||
function stringToDate(dateString: string): Date {
|
function stringToDate(dateString: string): Date {
|
||||||
const date = new Date(dateString.substring(0, 23));
|
const normalized = dateString.replace(/(\.\d{3})\d+/, "$1");
|
||||||
|
const withTz = /[Zz]$|[+-]\d{2}:\d{2}$|[+-]\d{4}$/.test(normalized) ? normalized : `${normalized}Z`;
|
||||||
|
const date = new Date(withTz);
|
||||||
if (isNaN(date.getTime())) throw new Error("Invalid date string");
|
if (isNaN(date.getTime())) throw new Error("Invalid date string");
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatLocalDate(dateString: string, options?: Intl.DateTimeFormatOptions): string {
|
||||||
|
return stringToDate(dateString).toLocaleDateString(undefined, options ?? { year: "numeric", month: "long", day: "numeric" });
|
||||||
|
}
|
||||||
|
|
||||||
function dateToTimeAgo(date: Date): string {
|
function dateToTimeAgo(date: Date): string {
|
||||||
const secondsAgo = Math.floor((Date.now() - date.getTime()) / 1000);
|
const secondsAgo = Math.floor((Date.now() - date.getTime()) / 1000);
|
||||||
const units: [number, string][] = [
|
const units: [number, string][] = [
|
||||||
@@ -32,4 +40,4 @@ function dateToTimeAgo(date: Date): string {
|
|||||||
return `${s} second${s !== 1 ? 's' : ''} ago`;
|
return `${s} second${s !== 1 ? 's' : ''} ago`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { formatTime, stringToDate, dateToTimeAgo };
|
export { formatTime, stringToDate, dateToTimeAgo, formatLocalDate };
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import java.util.Optional;
|
|||||||
@RequestMapping("/api/v1/stream")
|
@RequestMapping("/api/v1/stream")
|
||||||
public class StreamController {
|
public class StreamController {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(StreamController.class);
|
private static final Logger logger = LoggerFactory.getLogger(StreamController.class);
|
||||||
|
private static final String SUCCESS = "success";
|
||||||
|
|
||||||
private final StreamService streamService;
|
private final StreamService streamService;
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ public class StreamController {
|
|||||||
public ResponseEntity<APIResponse<Stream>> startStream(@RequestParam("name") String streamKey) {
|
public ResponseEntity<APIResponse<Stream>> startStream(@RequestParam("name") String streamKey) {
|
||||||
logger.info("Stream start callback for key: {}", streamKey);
|
logger.info("Stream start callback for key: {}", streamKey);
|
||||||
Stream stream = streamService.startStream(streamKey);
|
Stream stream = streamService.startStream(streamKey);
|
||||||
return ResponseEntity.ok(new APIResponse<>("success", "Stream started", stream));
|
return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Stream started", stream));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called by nginx on_publish_done
|
// Called by nginx on_publish_done
|
||||||
@@ -36,28 +37,28 @@ public class StreamController {
|
|||||||
public ResponseEntity<APIResponse<Void>> stopStream(@RequestParam("name") String streamKey) {
|
public ResponseEntity<APIResponse<Void>> stopStream(@RequestParam("name") String streamKey) {
|
||||||
logger.info("Stream stop callback for key: {}", streamKey);
|
logger.info("Stream stop callback for key: {}", streamKey);
|
||||||
streamService.endStream(streamKey);
|
streamService.endStream(streamKey);
|
||||||
return ResponseEntity.ok(new APIResponse<>("success", "Stream ended", null));
|
return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Stream ended", null));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called by nginx on_update
|
// Called by nginx on_update
|
||||||
@PostMapping("/heartbeat")
|
@PostMapping("/heartbeat")
|
||||||
public ResponseEntity<APIResponse<Void>> heartbeat(@RequestParam("name") String streamKey) {
|
public ResponseEntity<APIResponse<Void>> heartbeat(@RequestParam("name") String streamKey) {
|
||||||
streamService.heartbeatStream(streamKey);
|
streamService.heartbeatStream(streamKey);
|
||||||
return ResponseEntity.ok(new APIResponse<>("success", "Heartbeat recorded", null));
|
return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Heartbeat recorded", null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/history/{userId}")
|
@GetMapping("/history/{userId}")
|
||||||
public ResponseEntity<APIResponse<List<Stream>>> getStreamHistory(@PathVariable Long userId) {
|
public ResponseEntity<APIResponse<List<Stream>>> getStreamHistory(@PathVariable Long userId) {
|
||||||
List<Stream> history = streamService.getStreamHistory(userId);
|
List<Stream> history = streamService.getStreamHistory(userId);
|
||||||
return ResponseEntity.ok(new APIResponse<>("success", "Stream history retrieved", history));
|
return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Stream history retrieved", history));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/current")
|
@GetMapping("/current")
|
||||||
public ResponseEntity<APIResponse<StreamResponse>> getCurrentStream() {
|
public ResponseEntity<APIResponse<StreamResponse>> getCurrentStream() {
|
||||||
Optional<Stream> stream = streamService.getActiveStream();
|
Optional<Stream> stream = streamService.getActiveStream();
|
||||||
return stream.map(value -> ResponseEntity.ok(new APIResponse<>("success",
|
return stream.map(value -> ResponseEntity.ok(new APIResponse<>(SUCCESS,
|
||||||
"Streaming status retrieved",
|
"Streaming status retrieved",
|
||||||
new StreamResponse(true, value.getId())))).orElseGet(() -> ResponseEntity.ok(new APIResponse<>("success",
|
new StreamResponse(true, value.getId())))).orElseGet(() -> ResponseEntity.ok(new APIResponse<>(SUCCESS,
|
||||||
"Streaming status retrieved",
|
"Streaming status retrieved",
|
||||||
new StreamResponse(false, null))));
|
new StreamResponse(false, null))));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public class UserController {
|
|||||||
|
|
||||||
@Value("${jwt.expiration}")
|
@Value("${jwt.expiration}")
|
||||||
private long jwtExpiration;
|
private long jwtExpiration;
|
||||||
|
private static final String SUCCESS = "success";
|
||||||
|
|
||||||
public UserController(UserService userService) {
|
public UserController(UserService userService) {
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
@@ -34,7 +35,7 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
new APIResponse<>("success", "User retrieved successfully", user.get())
|
new APIResponse<>(SUCCESS, "User retrieved successfully", user.get())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ public class UserController {
|
|||||||
response.addHeader("Set-Cookie", cookie.toString());
|
response.addHeader("Set-Cookie", cookie.toString());
|
||||||
|
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
new APIResponse<>("success", "Logged in successfully", new Token(jwt))
|
new APIResponse<>(SUCCESS, "Logged in successfully", new Token(jwt))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ public class UserController {
|
|||||||
response.addHeader("Set-Cookie", cookie.toString());
|
response.addHeader("Set-Cookie", cookie.toString());
|
||||||
|
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
new APIResponse<>("success", "Logged out successfully", null)
|
new APIResponse<>(SUCCESS, "Logged out successfully", null)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,6 @@ import java.nio.file.Path;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -215,7 +214,7 @@ public class MediaService {
|
|||||||
return segMs < endMs && (segMs + HLS_SEGMENT_LENGTH * 1000) > startMs;
|
return segMs < endMs && (segMs + HLS_SEGMENT_LENGTH * 1000) > startMs;
|
||||||
})
|
})
|
||||||
.sorted(Comparator.comparingLong(this::parseTimestampMs))
|
.sorted(Comparator.comparingLong(this::parseTimestampMs))
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ public class UserService {
|
|||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
|
|
||||||
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||||
|
|
||||||
public UserService(UserRepository userRepository,
|
public UserService(UserRepository userRepository,
|
||||||
JwtService jwtService,
|
JwtService jwtService,
|
||||||
@Value("${google.client.id}") String googleClientId) {
|
@Value("${google.client.id}") String googleClientId) {
|
||||||
@@ -116,7 +118,7 @@ public class UserService {
|
|||||||
|
|
||||||
private String generateStreamKey() {
|
private String generateStreamKey() {
|
||||||
byte[] bytes = new byte[24];
|
byte[] bytes = new byte[24];
|
||||||
new SecureRandom().nextBytes(bytes);
|
SECURE_RANDOM.nextBytes(bytes);
|
||||||
return HexFormat.of().formatHex(bytes);
|
return HexFormat.of().formatHex(bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import java.util.concurrent.Future;
|
|||||||
public class MetadataService {
|
public class MetadataService {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(MetadataService.class);
|
private static final Logger logger = LoggerFactory.getLogger(MetadataService.class);
|
||||||
|
|
||||||
|
private static final String DURATION = "duration";
|
||||||
|
|
||||||
@Async("ffmpegTaskExecutor")
|
@Async("ffmpegTaskExecutor")
|
||||||
public Future<ClipOptions> getVideoMetadata(Path file) {
|
public Future<ClipOptions> getVideoMetadata(Path file) {
|
||||||
logger.info("Getting metadata for file {}", file);
|
logger.info("Getting metadata for file {}", file);
|
||||||
@@ -78,8 +80,8 @@ public class MetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Float extractDuration(JsonNode streamNode) {
|
private Float extractDuration(JsonNode streamNode) {
|
||||||
if (streamNode.has("duration")) {
|
if (streamNode.has(DURATION)) {
|
||||||
return Float.valueOf(streamNode.get("duration").asText());
|
return Float.valueOf(streamNode.get(DURATION).asText());
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new FFMPEGException("ffprobe duration missing");
|
throw new FFMPEGException("ffprobe duration missing");
|
||||||
@@ -115,8 +117,8 @@ public class MetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void extractEndPointFromFormat(ClipOptions metadata, JsonNode formatNode) {
|
private void extractEndPointFromFormat(ClipOptions metadata, JsonNode formatNode) {
|
||||||
if (formatNode != null && formatNode.has("duration") && metadata.getDuration() == null) {
|
if (formatNode != null && formatNode.has(DURATION) && metadata.getDuration() == null) {
|
||||||
metadata.setDuration(Float.parseFloat(formatNode.get("duration").asText()));
|
metadata.setDuration(Float.parseFloat(formatNode.get(DURATION).asText()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@@ -75,9 +76,13 @@ public class StreamActionsService {
|
|||||||
progressTracker.markComplete();
|
progressTracker.markComplete();
|
||||||
|
|
||||||
return CompletableFuture.completedFuture(outputFile);
|
return CompletableFuture.completedFuture(outputFile);
|
||||||
} catch (Exception e) {
|
} catch (IOException e) {
|
||||||
logger.error("Failed to save section to '{}'", outputFile, e);
|
logger.error("Failed to save section to '{}'", outputFile, e);
|
||||||
return CompletableFuture.failedFuture(e);
|
return CompletableFuture.failedFuture(e);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
logger.error("Failed to save section");
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return CompletableFuture.failedFuture(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user