diff --git a/.travis.yml b/.travis.yml index 1387060..50ace6d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ jdk: oraclejdk11 # We customize the script for compiling Maven application. For more about customizing the build: # https://docs.travis-ci.com/user/customizing-the-build script: - - ./mvnw install -DskipTests=true -Dmaven.javadoc.skip=true -B -V + - ./mvnw install -Dmaven.javadoc.skip=true -B -V before_install: - chmod +x mvnw diff --git a/CHANGELOG.md b/CHANGELOG.md index bb1eab8..67da197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +# 1.1.0 (2020-10-20) + +* refactor(media-streaming): rename class `MediaStreamingBootstrap` ([0ca94d0](https://github.com/johnnymillergh/media-streaming/commit/0ca94d0)) +* feat(FileWatcher): observe file changes on file system ([26ca2eb](https://github.com/johnnymillergh/media-streaming/commit/26ca2eb)) +* feat(Reactive): delete file observation ([e9ea525](https://github.com/johnnymillergh/media-streaming/commit/e9ea525)) +* perf(Exception): capture NoSuchFileException if the dir is wrong ([bd7c695](https://github.com/johnnymillergh/media-streaming/commit/bd7c695)) +* perf(Log): adjust log level ([f5d3264](https://github.com/johnnymillergh/media-streaming/commit/f5d3264)) +* perf(Log): print debug logs; enable log switch ([59f4789](https://github.com/johnnymillergh/media-streaming/commit/59f4789)) +* perf(media-streaming): assemble all necessary beans in class `MediaStreamingAutoConfiguration` ([d122439](https://github.com/johnnymillergh/media-streaming/commit/d122439)) +* perf(Route): reduce code warning ([a6530e1](https://github.com/johnnymillergh/media-streaming/commit/a6530e1)) +* perf(WebFlux): reactive VIDEO_CACHE data ([05ec18b](https://github.com/johnnymillergh/media-streaming/commit/05ec18b)) +* chore(starter): delete useless resource ([7c00b2b](https://github.com/johnnymillergh/media-streaming/commit/7c00b2b)) +* build(GitHub Actions): update trigger actions ([20b69cd](https://github.com/johnnymillergh/media-streaming/commit/20b69cd)) +* build(POM): update version to 1.1.0-SNAPSHOT ([4e69455](https://github.com/johnnymillergh/media-streaming/commit/4e69455)) +* build(Travis): don't skip tests ([ce6c6b9](https://github.com/johnnymillergh/media-streaming/commit/ce6c6b9)) +* test(sample-app): add unit tests for API ([888eb2c](https://github.com/johnnymillergh/media-streaming/commit/888eb2c)) +* test(sample-app): correct unit test for API ([1a367a6](https://github.com/johnnymillergh/media-streaming/commit/1a367a6)) +* fix(media-streaming): correct route bean factory ([bd9874e](https://github.com/johnnymillergh/media-streaming/commit/bd9874e)) +* docs: update README.md ([f5f2c84](https://github.com/johnnymillergh/media-streaming/commit/f5f2c84)) +* docs(sample-app): add comment for application.yml ([6d1336d](https://github.com/johnnymillergh/media-streaming/commit/6d1336d)) + + +### BREAKING CHANGE + +* observe file changes on file system +* print debug logs; enable log switch + + ## 1.0.1 (2020-10-19) * fix(POM): update and fix conflict version number ([cc2a201](https://github.com/johnnymillergh/media-streaming/commit/cc2a201)) diff --git a/README.md b/README.md index 9d95438..8c0c375 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ Spring Boot Starter - [![GitHub release](https://img.shields.io/github/release/johnnymillergh/media-streaming.svg)](https://github.com/johnnymillergh/media-streaming/releases) [![Build Status](https://travis-ci.com/johnnymillergh/media-streaming.svg?branch=master)](https://travis-ci.com/johnnymillergh/media-streaming) +![Maven Package](https://github.com/johnnymillergh/media-streaming/workflows/Maven%20Package/badge.svg?branch=master) [![GitHub issues](https://img.shields.io/github/issues/johnnymillergh/media-streaming)](https://github.com/johnnymillergh/media-streaming/issues) [![GitHub forks](https://img.shields.io/github/forks/johnnymillergh/media-streaming)](https://github.com/johnnymillergh/media-streaming/network) [![GitHub stars](https://img.shields.io/github/stars/johnnymillergh/media-streaming)](https://github.com/johnnymillergh/media-streaming) @@ -18,7 +18,7 @@ # Media Streaming -**Media Streaming**, a Spring Boot Starter project. +**Media Streaming**, a Spring Boot Starter project, which makes media streaming easier in your Spring Boot based project. ## Features @@ -28,21 +28,23 @@ Here is the highlights of **Media Streaming**: `org.springframework.boot:spring-boot-starter-parent` - [![Spring Boot](https://maven-badges.herokuapp.com/maven-central/org.springframework.boot/spring-boot-starter-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.springframework.boot/spring-boot-starter-parent/) -2. Docker support. - -6. API visualization. Enhanced Swagger API documentation. - -7. Log compression. +2. Media streaming over HTTP. (Basic media streaming ability was completed, more and more features is coming soon) -8. Request log. +3. … -9. Method Argument Validation Aspect. +## Installation -10. Docker container log persistence. +The easiest way is to install the library via its [Maven package](http://search.maven.org/#search|gav|1|g%3A"com.drewnoakes" AND a%3A"metadata-extractor"). -11. Startup statistics. +```xml + + com.jmframework.boot + media-streaming-spring-boot-starter + 1.0.1 + +``` -12. Customized startup banner. +Alternatively, download it from the [releases page](https://github.com/johnnymillergh/media-streaming/releases). ## Usage @@ -52,7 +54,7 @@ Here is the highlights of **Media Streaming**: $ git clone https://github.com/johnnymillergh/media-streaming.git ``` -2. Build with newest Intellij IDEA. +2. Build with the newest IntelliJ IDEA. 3. Click the green triangle to Run. diff --git a/media-streaming-sample-app/pom.xml b/media-streaming-sample-app/pom.xml index 4b464f4..694cf2c 100644 --- a/media-streaming-sample-app/pom.xml +++ b/media-streaming-sample-app/pom.xml @@ -5,7 +5,7 @@ com.jmframework.boot media-streaming - 1.0.1 + 1.1.0 com.jmsoftware.boot media-streaming-sample-app @@ -13,10 +13,12 @@ Media Streaming :: Sample App - - - - + com.jmframework.boot media-streaming-spring-boot-starter diff --git a/media-streaming-sample-app/src/main/java/com/jmsoftware/boot/mediastreamingsampleapp/MediaStreamingSampleAppApplication.java b/media-streaming-sample-app/src/main/java/com/jmsoftware/boot/mediastreamingsampleapp/MediaStreamingSampleAppApplication.java index 0f704ba..97c6b00 100644 --- a/media-streaming-sample-app/src/main/java/com/jmsoftware/boot/mediastreamingsampleapp/MediaStreamingSampleAppApplication.java +++ b/media-streaming-sample-app/src/main/java/com/jmsoftware/boot/mediastreamingsampleapp/MediaStreamingSampleAppApplication.java @@ -1,19 +1,34 @@ package com.jmsoftware.boot.mediastreamingsampleapp; +import lombok.extern.slf4j.Slf4j; +import lombok.val; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import java.time.Duration; +import java.time.Instant; +import java.util.TimeZone; + /** * Description: MediaStreamingSampleAppApplication, change description here. * * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com * date 10/16/2020 2:58 PM **/ +@Slf4j @SpringBootApplication public class MediaStreamingSampleAppApplication { + private static final String LINE_SEPARATOR = System.lineSeparator(); public static void main(String[] args) { + val startInstant = Instant.now(); SpringApplication.run(MediaStreamingSampleAppApplication.class, args); + val endInstant = Instant.now(); + val duration = Duration.between(startInstant, endInstant); + log.info("🥳 Congratulations! 🎉"); + log.info("🖥 {} started!", MediaStreamingSampleAppApplication.class.getSimpleName()); + log.info("⏳ Deployment duration: {} seconds ({} ms)", duration.getSeconds(), duration.toMillis()); + log.info("⏰ App started at {} (timezone - {})", endInstant, TimeZone.getDefault().getDisplayName()); + log.info("{} App running at{} - Local: http://localhost:8080/", LINE_SEPARATOR, LINE_SEPARATOR); } - } diff --git a/media-streaming-sample-app/src/main/resources/application.yml b/media-streaming-sample-app/src/main/resources/application.yml index 02b8d97..0f494bc 100644 --- a/media-streaming-sample-app/src/main/resources/application.yml +++ b/media-streaming-sample-app/src/main/resources/application.yml @@ -1,2 +1,7 @@ media-streaming: video-directory-on-file-system: 'C:\\Users\\Johnny\\Videos\\Mine' + +# Configure logging level of Media Streaming +logging: + level: + com.jmframework.boot.mediastreamingspringbootautoconfigure: DEBUG diff --git a/media-streaming-sample-app/src/main/resources/banner.txt b/media-streaming-sample-app/src/main/resources/banner.txt new file mode 100644 index 0000000..7829038 --- /dev/null +++ b/media-streaming-sample-app/src/main/resources/banner.txt @@ -0,0 +1,23 @@ +${AnsiStyle.BOLD}${AnsiColor.BRIGHT_GREEN} + __ + /|/| _ _/'_ ( _/_ _ _ _ ' _ +/ |(-(//(/ __)// (-(///)//)(/ + _/ + __ _ + ( _ _ /_ /_| +__)(///)/)((- ( |/)/) + / / / +${AnsiStyle.BOLD}Media Streaming Sample App :: Powered by Spring Boot ::${spring-boot.formatted-version} +${AnsiColor.CYAN}Author: Johnny Miller (鍾俊), email: johnnysviva@outlook.com +${AnsiStyle.NORMAL}${AnsiColor.MAGENTA}http://patorjk.com/software/taag/#p=display&f=Italic&t=Media%20Streaming%0ASample%20App +${AnsiColor.BRIGHT_BLACK} +-------------------------------------------Font Info------------------------------------------- +italic.flf Version 2 +by: Bas Meijer meijer@info.win.tue.nl bas@damek.kth.se +fixed by: Ryan Youck youck@cs.uregina.ca + +------------------------------------------Banner Info------------------------------------------ +Banner generated by Text ASCII Art Generator. +A web app that lets you type in large ASCII Art text lettering. +This can create art you can put in your email signature, on your webpage, etc etc. +More at http://patorjk.com/software/taag/ diff --git a/media-streaming-sample-app/src/test/java/com/jmsoftware/boot/mediastreamingsampleapp/MediaStreamingSampleAppApplicationTests.java b/media-streaming-sample-app/src/test/java/com/jmsoftware/boot/mediastreamingsampleapp/MediaStreamingSampleAppApplicationTests.java index e79363e..db62d0f 100644 --- a/media-streaming-sample-app/src/test/java/com/jmsoftware/boot/mediastreamingsampleapp/MediaStreamingSampleAppApplicationTests.java +++ b/media-streaming-sample-app/src/test/java/com/jmsoftware/boot/mediastreamingsampleapp/MediaStreamingSampleAppApplicationTests.java @@ -1,13 +1,40 @@ package com.jmsoftware.boot.mediastreamingsampleapp; +import com.jmframework.boot.mediastreamingspringbootautoconfigure.model.Video; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import java.util.List; + +/** + * Description: MediaStreamingSampleAppApplicationTests, change description here. + * + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 10/20/2020 1:04 PM + **/ +@Slf4j @SpringBootTest +@AutoConfigureWebTestClient class MediaStreamingSampleAppApplicationTests { + @Autowired + private WebTestClient webTestClient; - @Test - void contextLoads() { - } - + @Test + void videosTest() { + EntityExchangeResult> returnResult = webTestClient + .get() + .uri("/videos") + .accept(MediaType.ALL) + .exchange() + .expectStatus() + .isOk() + .expectBodyList(Video.class) + .returnResult(); + log.info("Video test: {}", returnResult.getResponseBody()); + } } diff --git a/media-streaming-spring-boot-autoconfigure/pom.xml b/media-streaming-spring-boot-autoconfigure/pom.xml index b20fdf0..3b04e23 100644 --- a/media-streaming-spring-boot-autoconfigure/pom.xml +++ b/media-streaming-spring-boot-autoconfigure/pom.xml @@ -5,7 +5,7 @@ com.jmframework.boot media-streaming - 1.0.1 + 1.1.0 media-streaming-spring-boot-autoconfigure Media Streaming :: Spring Boot Autoconfigure @@ -45,5 +45,10 @@ metadata-extractor 2.15.0 + + com.google.guava + guava + 29.0-jre + diff --git a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/api/VideoRoutes.java b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/api/VideoRoutes.java deleted file mode 100644 index 7e61a30..0000000 --- a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/api/VideoRoutes.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.jmframework.boot.mediastreamingspringbootautoconfigure.api; - -import com.jmframework.boot.mediastreamingspringbootautoconfigure.handler.VideoRouteHandler; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.function.server.RequestPredicate; -import org.springframework.web.reactive.function.server.RequestPredicates; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.ServerResponse; - -import static org.springframework.web.reactive.function.server.RequestPredicates.path; -import static org.springframework.web.reactive.function.server.RouterFunctions.route; - -/** - * Description: VideoRoutes, change description here. - * - * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 10/19/2020 4:38 PM - **/ -@Configuration -public class VideoRoutes { - @Bean - RouterFunction videoEndPoint(VideoRouteHandler videoRouteHandler) { - return route() - .nest(path("/videos"), builder -> builder.GET("", videoRouteHandler::listVideos) - .nest(path("/{name}"), videoBuilder -> videoBuilder.GET("", param("partial"), - videoRouteHandler::getPartialContent).GET("", videoRouteHandler::getFullContent) - ) - ).build(); - } - - private static RequestPredicate param(String parameter) { - return RequestPredicates.all().and(request -> request.queryParam(parameter).isPresent()); - } -} diff --git a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/Bootstrap.java b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/Bootstrap.java deleted file mode 100644 index df6cdb1..0000000 --- a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/Bootstrap.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.jmframework.boot.mediastreamingspringbootautoconfigure.configuration; - -import com.jmframework.boot.mediastreamingspringbootautoconfigure.model.Video; -import com.jmframework.boot.mediastreamingspringbootautoconfigure.repository.VideoRepository; -import com.jmframework.boot.mediastreamingspringbootautoconfigure.services.FileService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.CommandLineRunner; -import org.springframework.stereotype.Component; - -/** - * Description: Bootstrap, change description here. - * - * @author 钟俊, email: zhongjun@tuguide.cn, date: 10/19/2020 2:51 PM - **/ -@Slf4j -@Component -@RequiredArgsConstructor -public class Bootstrap implements CommandLineRunner { - private final VideoRepository videoRepository; - private final FileService fileService; - - @Override - public void run(String... args) { - fileService.getAllFiles() - .doOnNext(path -> log.debug("found file in path: " + path.toUri() + " FileName: " + path.getFileName())) - .flatMap(path -> { - Video video = new Video(); - video.setName(path.getFileName().toString()); - video.setLocation(path); - return videoRepository.addVideo(video); - }) - .subscribe(); - - videoRepository.getAllVideos() - .doOnNext(video -> log.info("Registered video: " + video.getName())) - .subscribe(); - } -} diff --git a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/MediaStreamingAutoConfiguration.java b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/MediaStreamingAutoConfiguration.java index 5b07467..d51a222 100644 --- a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/MediaStreamingAutoConfiguration.java +++ b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/MediaStreamingAutoConfiguration.java @@ -1,16 +1,33 @@ package com.jmframework.boot.mediastreamingspringbootautoconfigure.configuration; +import com.jmframework.boot.mediastreamingspringbootautoconfigure.handler.MediaStreamingExceptionHandler; +import com.jmframework.boot.mediastreamingspringbootautoconfigure.handler.VideoRouteHandler; +import com.jmframework.boot.mediastreamingspringbootautoconfigure.repository.VideoRepository; +import com.jmframework.boot.mediastreamingspringbootautoconfigure.repository.impl.InMemoryVideoOnFileSystemRepository; +import com.jmframework.boot.mediastreamingspringbootautoconfigure.services.FileService; +import com.jmframework.boot.mediastreamingspringbootautoconfigure.services.VideoService; +import com.jmframework.boot.mediastreamingspringbootautoconfigure.services.impl.FileServiceImpl; +import com.jmframework.boot.mediastreamingspringbootautoconfigure.services.impl.VideoServiceImpl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.server.RequestPredicate; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; import javax.annotation.PostConstruct; +import static org.springframework.web.reactive.function.server.RequestPredicates.path; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + /** * Description: MediaStreamingAutoConfiguration, change description here. * - * @author 钟俊, email: zhongjun@tuguide.cn, date: 10/19/2020 2:51 PM + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 10/19/2020 2:51 PM **/ @Slf4j @Configuration @@ -21,6 +38,73 @@ public class MediaStreamingAutoConfiguration { @PostConstruct public void afterInitialization() { - log.info("afterInitialization: {}", mediaStreamingProperties); + log.debug("MediaStreamingAutoConfiguration initialization is done. {}", mediaStreamingProperties); + } + + @Bean + @ConditionalOnMissingBean + public MediaStreamingBootstrap bootstrap(VideoRepository videoRepository, FileService fileService) { + return new MediaStreamingBootstrap(videoRepository, fileService, mediaStreamingProperties); + } + + @Bean + @ConditionalOnMissingBean + public WebFluxAutoConfiguration webFluxAutoConfiguration() { + return new WebFluxAutoConfiguration(); + } + + @Bean + @ConditionalOnMissingBean + public MediaStreamingExceptionHandler mediaStreamingExceptionHandler() { + return new MediaStreamingExceptionHandler(); + } + + @Bean + @ConditionalOnMissingBean + public VideoRouteHandler videoRouteHandler(VideoService videoService, FileService fileService) { + return new VideoRouteHandler(videoService, fileService); + } + + /** + * Video end point router function. + *

+ * TODO: solve Nullable Problems + * + * @param videoRouteHandler the video route handler + * @return the router function + */ + @Bean + @SuppressWarnings("NullableProblems") + public RouterFunction videoEndPoint(VideoRouteHandler videoRouteHandler) { + log.info("videoEndPoint"); + return route() + .nest(path("/videos"), builder -> builder.GET("", videoRouteHandler::listVideos) + .nest(path("/{name}"), videoBuilder -> videoBuilder.GET("", param("partial"), + videoRouteHandler::getPartialContent).GET("", videoRouteHandler::getFullContent) + ) + ).build(); + } + + @Bean + @ConditionalOnMissingBean + public VideoService videoService(VideoRepository videoRepository) { + return new VideoServiceImpl(videoRepository); + } + + @Bean + @ConditionalOnMissingBean + public FileService fileService() { + return new FileServiceImpl(mediaStreamingProperties); + } + + @Bean + @ConditionalOnMissingBean + public VideoRepository videoRepository() { + return new InMemoryVideoOnFileSystemRepository(); + } + + @SuppressWarnings("SameParameterValue") + private static RequestPredicate param(String parameter) { + return RequestPredicates.all().and(request -> request.queryParam(parameter).isPresent()); } } diff --git a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/MediaStreamingBootstrap.java b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/MediaStreamingBootstrap.java new file mode 100644 index 0000000..81f197d --- /dev/null +++ b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/MediaStreamingBootstrap.java @@ -0,0 +1,83 @@ +package com.jmframework.boot.mediastreamingspringbootautoconfigure.configuration; + +import com.jmframework.boot.mediastreamingspringbootautoconfigure.filewatch.FileWatcher; +import com.jmframework.boot.mediastreamingspringbootautoconfigure.filewatch.FileWatcherHandler; +import com.jmframework.boot.mediastreamingspringbootautoconfigure.model.Video; +import com.jmframework.boot.mediastreamingspringbootautoconfigure.repository.VideoRepository; +import com.jmframework.boot.mediastreamingspringbootautoconfigure.services.FileService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import reactor.core.publisher.Mono; + +import javax.annotation.PostConstruct; +import java.nio.file.Path; + +/** + * Description: MediaStreamingBootstrap, change description here. + * + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 10/19/2020 2:51 PM + **/ +@Slf4j +@RequiredArgsConstructor +public class MediaStreamingBootstrap implements CommandLineRunner { + private final VideoRepository videoRepository; + private final FileService fileService; + private final MediaStreamingProperties mediaStreamingProperties; + + @PostConstruct + private void postConstruct() { + log.debug("MediaStreamingBootstrap initialization is done. Start to process videos."); + log.debug("Starting FileWatcher..."); + FileWatcher fileWatcher; + try { + fileWatcher = new FileWatcher(mediaStreamingProperties.getVideoDirectoryOnFileSystem()); + } catch (Exception e) { + log.error("Cannot build FileWatcher, file observation failed! " + + "Check `media-streaming.videoDirectoryOnFileSystem` configuration.", e); + return; + } + fileWatcher.setFileWatcherHandler(new FileWatcherHandler() { + @Override + public void onCreated(Path file) { + log.debug("Created file observed: {}", file); + Video video = new Video(); + video.setName(file.getFileName().toString()); + video.setLocation(file); + Mono.just(video).then(videoRepository.addVideo(video)).subscribe(); + } + + @Override + public void onDeleted(Path file) { + log.debug("Deleted file observed: {}", file); + Mono.just(file).then(videoRepository.deleteVideoByPath(file)).subscribe(); + } + + @Override + public void onModified(Path file) { + log.info("Modified file observed: {}", file); + Video video = new Video(); + video.setName(file.getFileName().toString()); + video.setLocation(file); + Mono.just(video).then(videoRepository.addVideo(video)).subscribe(); + } + }); + } + + @Override + public void run(String... args) { + fileService.getAllFiles() + .doOnNext(path -> log.debug("found file in path: " + path.toUri() + " FileName: " + path.getFileName())) + .flatMap(path -> { + Video video = new Video(); + video.setName(path.getFileName().toString()); + video.setLocation(path); + return videoRepository.addVideo(video); + }) + .subscribe(); + + videoRepository.getAllVideos() + .doOnNext(video -> log.debug("Registered video: " + video.getName())) + .subscribe(); + } +} diff --git a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/ResourceRegionMessageWriter.java b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/ResourceRegionMessageWriter.java index c5ff414..b1467d4 100644 --- a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/ResourceRegionMessageWriter.java +++ b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/ResourceRegionMessageWriter.java @@ -46,7 +46,7 @@ public class ResourceRegionMessageWriter implements HttpMessageWriter> zeroCopy(Resource resource, ResourceRegion r return Optional.of(((ZeroCopyHttpOutputMessage) message).writeWith(file, position, count)); } catch (IOException e) { // Ignored + log.error("IO Exception occurred when zeroCopy", e); } } return Optional.empty(); @@ -76,8 +77,9 @@ private static long lengthOf(ResourceRegion resourceRegion) { if (resourceRegion.getResource().getClass() != InputStreamResource.class) { try { return resourceRegion.getResource().contentLength(); - } catch (Exception e) { + } catch (IOException e) { // Ignore Exception + log.error("IO Exception occurred when getting length of ResourceRegion.", e); } } return -1; @@ -184,7 +186,6 @@ public Mono write(Publisher inputStream, MediaType resourceMediaType = getResourceMediaType(mediaType, resourceRegion.getResource()); - headers.setContentType(resourceMediaType); headers.add(HttpHeaders.CONTENT_RANGE, "bytes " + startPosition + '-' + endPosition + '/' + contentLength); diff --git a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/WebFluxAutoConfiguration.java b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/WebFluxAutoConfiguration.java index 1d79594..44a9e2d 100644 --- a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/WebFluxAutoConfiguration.java +++ b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/configuration/WebFluxAutoConfiguration.java @@ -1,6 +1,5 @@ package com.jmframework.boot.mediastreamingspringbootautoconfigure.configuration; -import org.springframework.context.annotation.Configuration; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.config.WebFluxConfigurer; @@ -11,7 +10,6 @@ * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com * date 10/19/2020 2:33 PM **/ -@Configuration @EnableWebFlux public class WebFluxAutoConfiguration implements WebFluxConfigurer { @Override diff --git a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/filewatch/FileWatcher.java b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/filewatch/FileWatcher.java new file mode 100644 index 0000000..b79c8fa --- /dev/null +++ b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/filewatch/FileWatcher.java @@ -0,0 +1,97 @@ +package com.jmframework.boot.mediastreamingspringbootautoconfigure.filewatch; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import lombok.Setter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.PreDestroy; +import java.nio.file.*; +import java.util.Optional; +import java.util.concurrent.*; + +import static java.nio.file.StandardWatchEventKinds.*; + +/** + * Description: FileWatcher, change description here. + * + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 10/20/2020 3:19 PM + * @see Inspired by file-watcher + **/ +@Slf4j +@Setter +public class FileWatcher { + private static final ThreadFactory NAMED_THREAD_FACTORY = + new ThreadFactoryBuilder().setNameFormat("file-watcher-thread-%d").build(); + private static final ExecutorService THREAD_POOL = + new ThreadPoolExecutor(1, 2, 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(1024), NAMED_THREAD_FACTORY, new ThreadPoolExecutor.AbortPolicy()); + + private final Path monitoredPath; + private FileWatcherHandler fileWatcherHandler; + + public FileWatcher(String directory) { + this(Paths.get(directory)); + } + + @SneakyThrows + private FileWatcher(Path path) { + this.monitoredPath = path; + this.monitoredPath.register(WatchServiceSingleton.getInstance(), + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE, + StandardWatchEventKinds.ENTRY_MODIFY); + THREAD_POOL.execute(this::monitor); + } + + private void monitor() { + log.debug("Started watching: {}", this.monitoredPath); + while (true) { + // wait for key to be signaled + Optional optionalWatchKey = Optional.ofNullable(WatchServiceSingleton.getInstance().poll()); + if (optionalWatchKey.isPresent()) { + var watchKey = optionalWatchKey.get(); + for (var watchEvent : watchKey.pollEvents()) { + WatchEvent.Kind kind = watchEvent.kind(); + + // This key is registered only for ENTRY_CREATE events, + // but an OVERFLOW event can occur regardless if events are lost or discarded. + if (kind == OVERFLOW) { + continue; + } + + // The filename is the context of the event. + @SuppressWarnings("unchecked") + WatchEvent event = (WatchEvent) watchEvent; + Path filename = event.context(); + + // Resolve the filename against the directory. + // If the filename is "test" and the directory is "foo", the resolved name is "foo/test". + Path child = monitoredPath.resolve(filename); + if (kind == ENTRY_CREATE) { + fileWatcherHandler.onCreated(child); + } else if (kind == ENTRY_DELETE) { + fileWatcherHandler.onDeleted(child); + } else if (kind == ENTRY_MODIFY) { + fileWatcherHandler.onModified(child); + } + } + + // Reset the key -- this step is critical if you want to receive further watch events. + // If the key is no longer valid, the directory is inaccessible so exit the loop. + boolean valid = watchKey.reset(); + if (!valid) { + log.debug("The watch key wasn't valid. {}", watchKey); + return; + } + } + } + } + + @PreDestroy + @SneakyThrows + private void onPreDestroy() { + THREAD_POOL.awaitTermination(5, TimeUnit.SECONDS); + log.debug("THREAD_POOL for FileWatcher was terminated."); + } +} diff --git a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/filewatch/FileWatcherHandler.java b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/filewatch/FileWatcherHandler.java new file mode 100644 index 0000000..8a9bb91 --- /dev/null +++ b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/filewatch/FileWatcherHandler.java @@ -0,0 +1,31 @@ +package com.jmframework.boot.mediastreamingspringbootautoconfigure.filewatch; + +import java.nio.file.Path; + +/** + * Description: FileWatcherHandler, change description here. + * + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 10/20/2020 3:22 PM + */ +public interface FileWatcherHandler { + /** + * On created. + * + * @param file the file + */ + void onCreated(Path file); + + /** + * On deleted. + * + * @param file the file + */ + void onDeleted(Path file); + + /** + * On modified. + * + * @param file the file + */ + void onModified(Path file); +} diff --git a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/filewatch/WatchServiceSingleton.java b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/filewatch/WatchServiceSingleton.java new file mode 100644 index 0000000..f3cf74b --- /dev/null +++ b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/filewatch/WatchServiceSingleton.java @@ -0,0 +1,30 @@ +package com.jmframework.boot.mediastreamingspringbootautoconfigure.filewatch; + +import lombok.SneakyThrows; + +import java.nio.file.FileSystems; +import java.nio.file.WatchService; + +/** + * Description: WatchServiceSingleton, change description here. + * + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 10/20/2020 4:52 PM + **/ +public class WatchServiceSingleton { + private static volatile WatchService singletonInstance; + + private WatchServiceSingleton() { + } + + @SneakyThrows + public static WatchService getInstance() { + if (singletonInstance == null) { + synchronized (WatchServiceSingleton.class) { + if (singletonInstance == null) { + singletonInstance = FileSystems.getDefault().newWatchService(); + } + } + } + return singletonInstance; + } +} diff --git a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/handler/MediaStreamingExceptionHandler.java b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/handler/MediaStreamingExceptionHandler.java index 75af71d..e8983a6 100644 --- a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/handler/MediaStreamingExceptionHandler.java +++ b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/handler/MediaStreamingExceptionHandler.java @@ -10,7 +10,6 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.stereotype.Component; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.handler.WebFluxResponseStatusExceptionHandler; import org.springframework.web.server.ServerWebExchange; @@ -28,12 +27,11 @@ **/ @Slf4j @Order(-2) -@Component public class MediaStreamingExceptionHandler extends WebFluxResponseStatusExceptionHandler { final SimpleDateFormat simpleDate = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"); public MediaStreamingExceptionHandler() { - log.info("Initialized MediaStreamingExceptionHandler"); + log.debug("Initialized MediaStreamingExceptionHandler"); } @Override diff --git a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/handler/VideoRouteHandler.java b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/handler/VideoRouteHandler.java index fccd406..f1a0043 100644 --- a/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/handler/VideoRouteHandler.java +++ b/media-streaming-spring-boot-autoconfigure/src/main/java/com/jmframework/boot/mediastreamingspringbootautoconfigure/handler/VideoRouteHandler.java @@ -4,13 +4,12 @@ import com.jmframework.boot.mediastreamingspringbootautoconfigure.services.FileService; import com.jmframework.boot.mediastreamingspringbootautoconfigure.services.VideoService; import lombok.Data; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.core.io.UrlResource; import org.springframework.core.io.support.ResourceRegion; import org.springframework.http.CacheControl; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.Exceptions; @@ -22,24 +21,18 @@ * * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 10/19/2020 5:29 PM */ -@Component +@RequiredArgsConstructor public class VideoRouteHandler { private final VideoService videoService; private final FileService fileService; - @Autowired - public VideoRouteHandler(VideoService videoService, FileService fileService) { - this.videoService = videoService; - this.fileService = fileService; - } - - /** * List videos mono. * * @param request the request * @return the mono */ + public Mono listVideos(ServerRequest request) { Flux