feat(streaming): timestamp based stream markers #6
@@ -15,4 +15,6 @@ POSTGRES_PASSWORD=mypassword
|
|||||||
POSTGRES_DB=vodSystem
|
POSTGRES_DB=vodSystem
|
||||||
|
|
||||||
FRONTEND_URL=
|
FRONTEND_URL=
|
||||||
BACKEND_URL=
|
BACKEND_URL=
|
||||||
|
|
||||||
|
STREAM_RING_LENGTH_SECOND=28800
|
||||||
22
bruno/VoD-System/Markers/Create Marker.bru
Normal file
22
bruno/VoD-System/Markers/Create Marker.bru
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
meta {
|
||||||
|
name: Create Marker
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{base_url}}/markers
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"message": "Nice play"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
16
bruno/VoD-System/Markers/Get Marker.bru
Normal file
16
bruno/VoD-System/Markers/Get Marker.bru
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
meta {
|
||||||
|
name: Get Marker
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{base_url}}/markers/5
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
11
bruno/VoD-System/Markers/Get all markers.bru
Normal file
11
bruno/VoD-System/Markers/Get all markers.bru
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: Get all markers
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{base_url}}/markers
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
8
bruno/VoD-System/Markers/folder.bru
Normal file
8
bruno/VoD-System/Markers/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
meta {
|
||||||
|
name: Markers
|
||||||
|
seq: 6
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
mode: inherit
|
||||||
|
}
|
||||||
24
bruno/VoD-System/Media/Save Stream Section By Markers.bru
Normal file
24
bruno/VoD-System/Media/Save Stream Section By Markers.bru
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
meta {
|
||||||
|
name: Save Stream Section By Markers
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{base_url}}/media/save/markers
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"startMarkerId": 1,
|
||||||
|
"endMarkerId": 2,
|
||||||
|
"title": "Hello",
|
||||||
|
"description": "my clip"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
script:post-response {
|
||||||
|
bru.setEnvVar("uuid", res.getBody().data.uuid);
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ services:
|
|||||||
container_name: vod_rtmp
|
container_name: vod_rtmp
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- WEB_SERVER_URL=http://host.docker.internal:8080
|
- WEB_SERVER_URL=http://host.docker.internal:8080
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ services:
|
|||||||
context: ./nginx
|
context: ./nginx
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: vod_rtmp
|
container_name: vod_rtmp
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- WEB_SERVER_URL=http://backend:8080
|
- WEB_SERVER_URL=http://backend:8080
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ rtmp {
|
|||||||
hls_path /stream;
|
hls_path /stream;
|
||||||
hls_fragment_naming system;
|
hls_fragment_naming system;
|
||||||
hls_fragment 3;
|
hls_fragment 3;
|
||||||
hls_playlist_length 28800;
|
hls_playlist_length ${STREAM_RING_LENGTH_SECOND};
|
||||||
|
|
||||||
on_publish ${WEB_SERVER_URL}/api/v1/stream/start;
|
on_publish ${WEB_SERVER_URL}/api/v1/stream/start;
|
||||||
on_publish_done ${WEB_SERVER_URL}/api/v1/stream/stop;
|
on_publish_done ${WEB_SERVER_URL}/api/v1/stream/stop;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# must only run on certain env files, otherwise it replaces too much
|
# must only run on certain env files, otherwise it replaces too much
|
||||||
envsubst '${WEB_SERVER_URL}' \
|
envsubst '${WEB_SERVER_URL} ${STREAM_RING_LENGTH_SECOND}' \
|
||||||
< /etc/nginx/templates/default.conf.template \
|
< /etc/nginx/templates/default.conf.template \
|
||||||
> /etc/nginx/nginx.conf
|
> /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
|||||||
@@ -108,4 +108,18 @@ public class GlobalExceptionHandler {
|
|||||||
APIResponse<Void> response = new APIResponse<>(ERROR, ex.getMessage(), null);
|
APIResponse<Void> response = new APIResponse<>(ERROR, ex.getMessage(), null);
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(NotStreaming.class)
|
||||||
|
public ResponseEntity<APIResponse<Void>> handleNotStreaming(NotStreaming ex) {
|
||||||
|
logger.error("NotStreaming: {}", ex.getMessage());
|
||||||
|
APIResponse<Void> response = new APIResponse<>(ERROR, ex.getMessage(), null);
|
||||||
|
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MarkerNotFound.class)
|
||||||
|
public ResponseEntity<APIResponse<Void>> handleMarkerNotFound(MarkerNotFound ex) {
|
||||||
|
logger.error("MarkerNotFound: {}", ex.getMessage());
|
||||||
|
APIResponse<Void> response = new APIResponse<>(ERROR, ex.getMessage(), null);
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.ddf.vodsystem.controllers;
|
||||||
|
|
||||||
|
import com.ddf.vodsystem.controllers.dto.MarkerCreateRequest;
|
||||||
|
import com.ddf.vodsystem.controllers.dto.MarkerResponse;
|
||||||
|
import com.ddf.vodsystem.dto.APIResponse;
|
||||||
|
import com.ddf.vodsystem.entities.Marker;
|
||||||
|
import com.ddf.vodsystem.services.MarkerService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/markers")
|
||||||
|
public class MarkerController {
|
||||||
|
private static final String SUCCESS = "success";
|
||||||
|
|
||||||
|
private final MarkerService markerService;
|
||||||
|
|
||||||
|
public MarkerController(MarkerService markerService) {
|
||||||
|
this.markerService = markerService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("")
|
||||||
|
public ResponseEntity<APIResponse<List<MarkerResponse>>> getMarkers() {
|
||||||
|
List<MarkerResponse> markers = markerService.getUserMarkers().stream()
|
||||||
|
.map(this::convertToDTO)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
new APIResponse<>(SUCCESS, "Markers retrieved successfully", markers)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<APIResponse<MarkerResponse>> getMarkerById(@PathVariable Long id) {
|
||||||
|
Marker marker = markerService.getMarkerById(id);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
new APIResponse<>(SUCCESS, "Marker retrieved successfully", convertToDTO(marker))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("")
|
||||||
|
public ResponseEntity<APIResponse<MarkerResponse>> createMarker(@Valid @RequestBody MarkerCreateRequest request) {
|
||||||
|
Marker marker = markerService.create(request.message());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
new APIResponse<>(SUCCESS, "Marker created successfully", convertToDTO(marker))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MarkerResponse convertToDTO(Marker marker) {
|
||||||
|
return new MarkerResponse(
|
||||||
|
marker.getId(),
|
||||||
|
marker.getUser().getId(),
|
||||||
|
marker.getStream().getId(),
|
||||||
|
marker.getMessage(),
|
||||||
|
marker.getTimestamp()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ddf.vodsystem.controllers;
|
package com.ddf.vodsystem.controllers;
|
||||||
|
|
||||||
import com.ddf.vodsystem.controllers.dto.ClipSectionRequest;
|
import com.ddf.vodsystem.controllers.dto.ClipSectionRequest;
|
||||||
|
import com.ddf.vodsystem.controllers.dto.SaveSectionByMarkerRequest;
|
||||||
import com.ddf.vodsystem.controllers.dto.SaveSectionRequest;
|
import com.ddf.vodsystem.controllers.dto.SaveSectionRequest;
|
||||||
import com.ddf.vodsystem.controllers.dto.UUIDResponse;
|
import com.ddf.vodsystem.controllers.dto.UUIDResponse;
|
||||||
import com.ddf.vodsystem.dto.Job;
|
import com.ddf.vodsystem.dto.Job;
|
||||||
@@ -54,6 +55,21 @@ public class MediaController {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/save/markers")
|
||||||
|
public ResponseEntity<APIResponse<UUIDResponse>> saveByMarkers(
|
||||||
|
@Valid @RequestBody SaveSectionByMarkerRequest saveSectionByMarkerRequest) throws IOException {
|
||||||
|
Job job = mediaService.saveSection(
|
||||||
|
saveSectionByMarkerRequest.startMarkerId(),
|
||||||
|
saveSectionByMarkerRequest.endMarkerId(),
|
||||||
|
saveSectionByMarkerRequest.title(),
|
||||||
|
saveSectionByMarkerRequest.description());
|
||||||
|
return ResponseEntity.ok(new APIResponse<>(
|
||||||
|
SUCCESS,
|
||||||
|
"Section saving successfully started",
|
||||||
|
new UUIDResponse(job.getUuid())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/clip")
|
@PostMapping("/clip")
|
||||||
public ResponseEntity<APIResponse<UUIDResponse>> clip(
|
public ResponseEntity<APIResponse<UUIDResponse>> clip(
|
||||||
@RequestBody ClipSectionRequest clipSectionRequest) throws IOException {
|
@RequestBody ClipSectionRequest clipSectionRequest) throws IOException {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ddf.vodsystem.controllers.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record MarkerCreateRequest(
|
||||||
|
@NotBlank String message
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.ddf.vodsystem.controllers.dto;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
public record MarkerResponse(
|
||||||
|
Long id,
|
||||||
|
Long userId,
|
||||||
|
Long streamId,
|
||||||
|
String message,
|
||||||
|
Instant timestamp
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.ddf.vodsystem.controllers.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public record SaveSectionByMarkerRequest(
|
||||||
|
@NotNull Long startMarkerId,
|
||||||
|
@NotNull Long endMarkerId,
|
||||||
|
String title,
|
||||||
|
String description) {}
|
||||||
33
src/main/java/com/ddf/vodsystem/entities/Marker.java
Normal file
33
src/main/java/com/ddf/vodsystem/entities/Marker.java
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ddf.vodsystem.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "markers")
|
||||||
|
@Data
|
||||||
|
public class Marker {
|
||||||
|
@Id
|
||||||
|
@Column(name = "id", nullable = false, unique = true)
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JsonIgnore
|
||||||
|
@JoinColumn(name = "user_id", nullable = false)
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JsonIgnore
|
||||||
|
@JoinColumn(name = "stream_id", nullable = false)
|
||||||
|
private Stream stream;
|
||||||
|
|
||||||
|
@Column(name = "message", length = 255)
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
@Column(name = "timestamp")
|
||||||
|
private Instant timestamp;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ddf.vodsystem.exceptions;
|
||||||
|
|
||||||
|
public class MarkerNotFound extends RuntimeException {
|
||||||
|
public MarkerNotFound(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ddf.vodsystem.exceptions;
|
||||||
|
|
||||||
|
public class NotStreaming extends RuntimeException {
|
||||||
|
public NotStreaming(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.ddf.vodsystem.repositories;
|
||||||
|
|
||||||
|
import com.ddf.vodsystem.entities.Marker;
|
||||||
|
import com.ddf.vodsystem.entities.User;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface MarkerRepository extends JpaRepository<Marker, Long> {
|
||||||
|
@Query("SELECT m FROM Marker m WHERE m.user = ?1")
|
||||||
|
List<Marker> findByUser(User user);
|
||||||
|
|
||||||
|
@Query("SELECT m FROM Marker m WHERE m.timestamp <= ?1")
|
||||||
|
List<Marker> findAllBefore(Instant time);
|
||||||
|
}
|
||||||
@@ -174,7 +174,7 @@ public class ClipService {
|
|||||||
Path file = directoryService.resolvePath(path);
|
Path file = directoryService.resolvePath(path);
|
||||||
|
|
||||||
if (!Files.exists(file)) {
|
if (!Files.exists(file)) {
|
||||||
throw new JobNotFound("Thumbnail file not found");
|
throw new ClipNotFound("Thumbnail file not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new FileSystemResource(file);
|
return new FileSystemResource(file);
|
||||||
|
|||||||
82
src/main/java/com/ddf/vodsystem/services/MarkerService.java
Normal file
82
src/main/java/com/ddf/vodsystem/services/MarkerService.java
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package com.ddf.vodsystem.services;
|
||||||
|
|
||||||
|
import com.ddf.vodsystem.entities.Marker;
|
||||||
|
import com.ddf.vodsystem.entities.Stream;
|
||||||
|
import com.ddf.vodsystem.entities.User;
|
||||||
|
import com.ddf.vodsystem.exceptions.MarkerNotFound;
|
||||||
|
import com.ddf.vodsystem.exceptions.NotAuthenticated;
|
||||||
|
import com.ddf.vodsystem.exceptions.NotStreaming;
|
||||||
|
import com.ddf.vodsystem.repositories.MarkerRepository;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MarkerService {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
private final StreamService streamService;
|
||||||
|
private final MarkerRepository markerRepository;
|
||||||
|
|
||||||
|
@Value("${stream.max-length}")
|
||||||
|
private Long maxStreamLength;
|
||||||
|
|
||||||
|
public MarkerService(UserService userService, StreamService streamService, MarkerRepository markerRepository) {
|
||||||
|
this.userService = userService;
|
||||||
|
this.streamService = streamService;
|
||||||
|
this.markerRepository = markerRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Marker getMarkerById(Long id) {
|
||||||
|
Optional<Marker> marker = markerRepository.findById(id);
|
||||||
|
|
||||||
|
if (marker.isEmpty()) {
|
||||||
|
throw new MarkerNotFound("Marker does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<User> user = userService.getLoggedInUser();
|
||||||
|
if (user.isEmpty() || !user.get().equals(marker.get().getUser())) {
|
||||||
|
throw new NotAuthenticated("User not authenticated for this marker");
|
||||||
|
}
|
||||||
|
|
||||||
|
return marker.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Marker> getUserMarkers() {
|
||||||
|
Optional<User> user = userService.getLoggedInUser();
|
||||||
|
|
||||||
|
if (user.isEmpty()) {
|
||||||
|
throw new NotAuthenticated("Must be logged in to get markers");
|
||||||
|
}
|
||||||
|
|
||||||
|
return markerRepository.findByUser(user.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Marker create(String message) {
|
||||||
|
Optional<Stream> stream = streamService.getActiveStream();
|
||||||
|
|
||||||
|
if (stream.isEmpty()) {
|
||||||
|
throw new NotStreaming("User must be streaming to mark");
|
||||||
|
}
|
||||||
|
|
||||||
|
Marker marker = new Marker();
|
||||||
|
marker.setStream(stream.get());
|
||||||
|
marker.setUser(stream.get().getUser());
|
||||||
|
marker.setMessage(message);
|
||||||
|
marker.setTimestamp(Instant.now());
|
||||||
|
return markerRepository.saveAndFlush(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(fixedDelay = 360_000)
|
||||||
|
@Transactional
|
||||||
|
public void deleteOldMarkers() {
|
||||||
|
Instant cutoff = Instant.now().minusSeconds(maxStreamLength);
|
||||||
|
List<Marker> stale = markerRepository.findAllBefore(cutoff);
|
||||||
|
markerRepository.deleteAllInBatch(stale);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ package com.ddf.vodsystem.services;
|
|||||||
import com.ddf.vodsystem.dto.ClipOptions;
|
import com.ddf.vodsystem.dto.ClipOptions;
|
||||||
import com.ddf.vodsystem.dto.Job;
|
import com.ddf.vodsystem.dto.Job;
|
||||||
import com.ddf.vodsystem.dto.JobState;
|
import com.ddf.vodsystem.dto.JobState;
|
||||||
|
import com.ddf.vodsystem.entities.Marker;
|
||||||
import com.ddf.vodsystem.entities.User;
|
import com.ddf.vodsystem.entities.User;
|
||||||
|
import com.ddf.vodsystem.exceptions.MarkerNotFound;
|
||||||
import com.ddf.vodsystem.exceptions.NotAuthenticated;
|
import com.ddf.vodsystem.exceptions.NotAuthenticated;
|
||||||
import com.ddf.vodsystem.services.media.CompressionService;
|
import com.ddf.vodsystem.services.media.CompressionService;
|
||||||
import com.ddf.vodsystem.services.media.StreamActionsService;
|
import com.ddf.vodsystem.services.media.StreamActionsService;
|
||||||
@@ -32,12 +34,13 @@ public class MediaService {
|
|||||||
private static final int HLS_SEGMENT_LENGTH = 3;
|
private static final int HLS_SEGMENT_LENGTH = 3;
|
||||||
private final StreamActionsService streamActionsService;
|
private final StreamActionsService streamActionsService;
|
||||||
private final VodService vodService;
|
private final VodService vodService;
|
||||||
|
private final MarkerService markerService;
|
||||||
|
|
||||||
public MediaService(JobRegistryService jobRegistryService,
|
public MediaService(JobRegistryService jobRegistryService,
|
||||||
DirectoryService directoryService,
|
DirectoryService directoryService,
|
||||||
CompressionService compressionService,
|
CompressionService compressionService,
|
||||||
UserService userService,
|
UserService userService,
|
||||||
ClipService clipService, StreamActionsService streamActionsService, VodService vodService) {
|
ClipService clipService, StreamActionsService streamActionsService, VodService vodService, MarkerService markerService) {
|
||||||
this.jobRegistryService = jobRegistryService;
|
this.jobRegistryService = jobRegistryService;
|
||||||
this.directoryService = directoryService;
|
this.directoryService = directoryService;
|
||||||
this.compressionService = compressionService;
|
this.compressionService = compressionService;
|
||||||
@@ -45,6 +48,7 @@ public class MediaService {
|
|||||||
this.clipService = clipService;
|
this.clipService = clipService;
|
||||||
this.streamActionsService = streamActionsService;
|
this.streamActionsService = streamActionsService;
|
||||||
this.vodService = vodService;
|
this.vodService = vodService;
|
||||||
|
this.markerService = markerService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,6 +153,37 @@ public class MediaService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a section of the currently authenticated user's live stream between two markers.
|
||||||
|
* <p>
|
||||||
|
* Resolves {@code startMarkerId} and {@code endMarkerId} to their marker timestamps and
|
||||||
|
* delegates to {@link #saveSection(Instant, Instant, String, String)}.
|
||||||
|
*
|
||||||
|
* @param startMarkerId id of the marker whose timestamp begins the section to save
|
||||||
|
* @param endMarkerId id of the marker whose timestamp ends the section to save
|
||||||
|
* @param title title for the saved VoD; defaults to the current timestamp if {@code null}
|
||||||
|
* @param description description for the saved VoD; defaults to an empty string if {@code null}
|
||||||
|
* @return the {@link Job} tracking the save; state transitions to
|
||||||
|
* {@link JobState#SUCCEEDED} or {@link JobState#FAILED} asynchronously
|
||||||
|
* @throws MarkerNotFound if either marker id does not exist
|
||||||
|
* @throws NotAuthenticated if no user is currently authenticated, or the caller does not
|
||||||
|
* own one of the referenced markers
|
||||||
|
* @throws IllegalArgumentException if the start marker's timestamp is not before the end
|
||||||
|
* marker's, or if no stream segments exist in the given range
|
||||||
|
* @throws IOException if reading the stream directory or its segments fails
|
||||||
|
*/
|
||||||
|
public Job saveSection(
|
||||||
|
Long startMarkerId,
|
||||||
|
Long endMarkerId,
|
||||||
|
String title,
|
||||||
|
String description
|
||||||
|
) throws IOException {
|
||||||
|
Marker start = markerService.getMarkerById(startMarkerId);
|
||||||
|
Marker end = markerService.getMarkerById(endMarkerId);
|
||||||
|
|
||||||
|
return saveSection(start.getTimestamp(), end.getTimestamp(), title, description);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the last {@code duration} seconds of the currently authenticated user's live stream as a clip.
|
* Saves the last {@code duration} seconds of the currently authenticated user's live stream as a clip.
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ spring.application.name=vodSystem
|
|||||||
# VODs
|
# VODs
|
||||||
spring.servlet.multipart.max-file-size=2GB
|
spring.servlet.multipart.max-file-size=2GB
|
||||||
spring.servlet.multipart.max-request-size=2GB
|
spring.servlet.multipart.max-request-size=2GB
|
||||||
|
stream.max-length=${STREAM_RING_LENGTH_SECOND}
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
spring.datasource.url=${POSTGRES_URL}
|
spring.datasource.url=${POSTGRES_URL}
|
||||||
|
|||||||
@@ -54,4 +54,15 @@ CREATE TABLE IF NOT EXISTS vods (
|
|||||||
video_path VARCHAR(255) NOT NULL,
|
video_path VARCHAR(255) NOT NULL,
|
||||||
thumbnail_path VARCHAR(255) NOT NULL,
|
thumbnail_path VARCHAR(255) NOT NULL,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS markers (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
stream_id BIGINT NOT NULL,
|
||||||
|
message VARCHAR(255),
|
||||||
|
timestamp TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (stream_id) REFERENCES streams(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
Reference in New Issue
Block a user