Repository: saravanastar/video-streaming Branch: master Commit: 4cad3405235c Files: 25 Total size: 55.8 KB Directory structure: gitextract_70r0erqr/ ├── .gitignore ├── .mvn/ │ └── wrapper/ │ └── maven-wrapper.properties ├── Dockerfile ├── README.md ├── docker-compose.yaml ├── mvnw ├── mvnw.cmd ├── pom.xml └── src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── ask/ │ │ └── home/ │ │ └── videostream/ │ │ ├── Application.java │ │ ├── adapter/ │ │ │ ├── ContentAdapter.java │ │ │ └── LocalFileContentAdapter.java │ │ ├── config/ │ │ │ ├── ApplicationConfig.java │ │ │ └── VideoStreamConfig.java │ │ ├── constants/ │ │ │ └── ApplicationConstants.java │ │ ├── controller/ │ │ │ └── VideoController.java │ │ ├── model/ │ │ │ ├── Content.java │ │ │ └── ContentRequest.java │ │ ├── service/ │ │ │ └── VideoService.java │ │ └── util/ │ │ └── FileUtil.java │ └── resources/ │ └── application.yml └── test/ ├── java/ │ └── com/ │ └── ask/ │ └── home/ │ └── videostream/ │ ├── ApplicationTests.java │ ├── adapter/ │ │ └── LocalFileContentAdapterTest.java │ ├── controller/ │ │ └── VideoControllerTest.java │ └── service/ │ └── VideoServiceTest.java └── resources/ └── application-test.yml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/** !**/src/test/** ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ build/ ### VS Code ### .vscode/ ================================================ FILE: .mvn/wrapper/maven-wrapper.properties ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. wrapperVersion=3.3.2 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip ================================================ FILE: Dockerfile ================================================ FROM maven:3.9.9-amazoncorretto-23-alpine AS build LABEL authors="Saravanakumar Arunachalam" COPY . . RUN mvn clean package FROM amazoncorretto:23-jdk WORKDIR /app COPY --from=build target/video-stream.jar . COPY --from=build target/classes/video/toystory.mp4 ./video/ EXPOSE 8080 ENTRYPOINT java $JAVA_OPTS -jar ./video-stream.jar ================================================ FILE: README.md ================================================ # Video Service Overview This service is designed to manage video files by providing CRUD (Create, Read, Update, Delete) operations and enabling HTTP-based streaming of videos. ## Features - **Video Management**: Perform CRUD operations on video files. - **HTTP Streaming**: Stream video content directly over HTTP. ## Local Setup To run the service locally, you can launch it either as a Java application or within a Docker container. ### Configuration - **Port**: The application listens on port `8080` by default. - **Context Path and API Endpoint**: The base context path is `/video-service`, with an API prefix of `/api/v1/videos`. ### Prerequisites Ensure the following dependencies are installed: - Java 17 or higher - Docker and Docker Compose (if running in a containerized environment) ### Running the Application #### Using Java 1. Clone the repository: ```bash git clone cd ``` 2. Build and run the application using your preferred IDE or a build tool like Maven: ```bash mvn spring-boot:run ``` #### Using Docker 1. Verify that Docker and Docker Compose are installed on your system. If not, refer to the official installation guides for [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/). 2. Build and run the Docker container: ```bash docker-compose build docker-compose up ``` #### Adding Video Files - **Default Directory**: The application uses the `./video` directory for video content by default. - **Custom Directory**: To use a custom directory, set the `VIDEO_CONTENT_PATH` environment variable to point to your desired location. - **Mounting Volumes**: When running in Docker, mount the desired video folder as a volume in the container to include additional video files. Example of mounting a directory: docker-compose.yaml ```yaml services: video-service: volumes: - /path/to/local/video/folder:/tmp/content ``` ## Streaming Endpoint The application provides an HTTP streaming endpoint to serve video content. ### Endpoint Format - URL: `/video-service/api/v1/videos` - Will list the all video files in the directory - URL: `/video-service/api/v1/videos/stream/{fileType}/{fileName}` - **`fileType`**: The format of the video file (e.g., `mp4`, `mkv`). - **`fileName`**: The name of the video file without the extension. - URL: `/video-service/api/v1/videos/object-key/{objectKey}` - prefer to use this endpoint - **`objectKey`**: Object key of the file, can get the object key by hitting the `/video-service/api/v1/videos` ### Example Request Once the application is running, you can access the streaming service with the following format: ``` http://{host}:{port}/video-service/api/v1/videos/stream/{fileType}/{fileName} ``` For example: ``` http://localhost:8080/video-service/api/v1/videos/stream/mp4/toystory ``` ### Customizing the Video Directory To change the default video content directory: 1. Set the `VIDEO_CONTENT_PATH` environment variable. ```bash export VIDEO_CONTENT_PATH=/path/to/your/video/directory ``` 2. Restart the application to apply the changes. ## Testing and Debugging ### Unit and Integration Tests - The project includes unit and integration tests to ensure functionality. Use the following command to execute tests: ```bash mvn test ``` ### Logging - The application uses configurable logging via `logback.xml`. Logs are stored in the `logs` directory by default. - To change the logging level, modify the `application.properties` file or the `logback.xml` configuration. ### Common Issues 1. **Port Already in Use**: - If port `8080` is occupied, modify the port in the `application.properties` file: ```properties server.port=9090 ``` 2. **File Not Found**: - Ensure that video files are present in the specified directory. - Verify the `VIDEO_CONTENT_PATH` environment variable if using a custom directory. ## Further Reading For an in-depth explanation of how this service is implemented, including details on HTTP-based video streaming using Spring Boot, check out [this Medium article](https://medium.com/@saravanastar/video-streaming-over-http-using-spring-boot-51e9830a3b8). ================================================ FILE: docker-compose.yaml ================================================ services: video-service: build: context: ./ dockerfile: Dockerfile volumes: - ./video:/tmp/content environment: VIDEO_CONTENT_PATH: /tmp/content ports: - 8080:8080 ================================================ FILE: mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Apache Maven Wrapper startup batch script, version 3.3.2 # # Optional ENV vars # ----------------- # JAVA_HOME - location of a JDK home dir, required when download maven via java source # MVNW_REPOURL - repo url base for downloading maven distribution # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- set -euf [ "${MVNW_VERBOSE-}" != debug ] || set -x # OS specific support. native_path() { printf %s\\n "$1"; } case "$(uname)" in CYGWIN* | MINGW*) [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" native_path() { cygpath --path --windows "$1"; } ;; esac # set JAVACMD and JAVACCMD set_java_home() { # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched if [ -n "${JAVA_HOME-}" ]; then if [ -x "$JAVA_HOME/jre/sh/java" ]; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" JAVACCMD="$JAVA_HOME/jre/sh/javac" else JAVACMD="$JAVA_HOME/bin/java" JAVACCMD="$JAVA_HOME/bin/javac" if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 return 1 fi fi else JAVACMD="$( 'set' +e 'unset' -f command 2>/dev/null 'command' -v java )" || : JAVACCMD="$( 'set' +e 'unset' -f command 2>/dev/null 'command' -v javac )" || : if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 return 1 fi fi } # hash string like Java String::hashCode hash_string() { str="${1:-}" h=0 while [ -n "$str" ]; do char="${str%"${str#?}"}" h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) str="${str#?}" done printf %x\\n $h } verbose() { :; } [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } die() { printf %s\\n "$1" >&2 exit 1 } trim() { # MWRAPPER-139: # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. # Needed for removing poorly interpreted newline sequences when running in more # exotic environments such as mingw bash on Windows. printf "%s" "${1}" | tr -d '[:space:]' } # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; :Linux*x86_64*) distributionPlatform=linux-amd64 ;; *) echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 distributionPlatform=linux-amd64 ;; esac distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" distributionUrlName="${distributionUrl##*/}" distributionUrlNameMain="${distributionUrlName%.*}" distributionUrlNameMain="${distributionUrlNameMain%-bin}" MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" exec_maven() { unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" } if [ -d "$MAVEN_HOME" ]; then verbose "found existing MAVEN_HOME at $MAVEN_HOME" exec_maven "$@" fi case "${distributionUrl-}" in *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; esac # prepare tmp dir if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } trap clean HUP INT TERM EXIT else die "cannot create temp dir" fi mkdir -p -- "${MAVEN_HOME%/*}" # Download and Install Apache Maven verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." verbose "Downloading from: $distributionUrl" verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" # select .zip or .tar.gz if ! command -v unzip >/dev/null; then distributionUrl="${distributionUrl%.zip}.tar.gz" distributionUrlName="${distributionUrl##*/}" fi # verbose opt __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v # normalize http auth case "${MVNW_PASSWORD:+has-password}" in '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; esac if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then verbose "Found wget ... using wget" wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then verbose "Found curl ... using curl" curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" elif set_java_home; then verbose "Falling back to use Java to download" javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" cat >"$javaSource" <<-END public class Downloader extends java.net.Authenticator { protected java.net.PasswordAuthentication getPasswordAuthentication() { return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); } public static void main( String[] args ) throws Exception { setDefault( new Downloader() ); java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); } } END # For Cygwin/MinGW, switch paths to Windows format before running javac and java verbose " - Compiling Downloader.java ..." "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" verbose " - Running Downloader.java ..." "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" fi # If specified, validate the SHA-256 sum of the Maven distribution zip file if [ -n "${distributionSha256Sum-}" ]; then distributionSha256Result=false if [ "$MVN_CMD" = mvnd.sh ]; then echo "Checksum validation is not supported for maven-mvnd." >&2 echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then distributionSha256Result=true fi else echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 fi if [ $distributionSha256Result = false ]; then echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 exit 1 fi fi # unzip and move if command -v unzip >/dev/null; then unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" ================================================ FILE: mvnw.cmd ================================================ <# : batch portion @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM http://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Apache Maven Wrapper startup batch script, version 3.3.2 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output @REM ---------------------------------------------------------------------------- @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) @SET __MVNW_CMD__= @SET __MVNW_ERROR__= @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% @SET PSModulePath= @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) ) @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% @SET __MVNW_PSMODULEP_SAVE= @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> $ErrorActionPreference = "Stop" if ($env:MVNW_VERBOSE -eq "true") { $VerbosePreference = "Continue" } # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl if (!$distributionUrl) { Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" } switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { "maven-mvnd-*" { $USE_MVND = $true $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" $MVN_CMD = "mvnd.cmd" break } default { $USE_MVND = $false $MVN_CMD = $script -replace '^mvnw','mvn' break } } # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" if ($env:MAVEN_USER_HOME) { $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" } $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" exit $? } if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" } # prepare tmp dir $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null trap { if ($TMP_DOWNLOAD_DIR.Exists) { try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } } } New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null # Download and Install Apache Maven Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." Write-Verbose "Downloading from: $distributionUrl" Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" $webclient = New-Object System.Net.WebClient if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null # If specified, validate the SHA-256 sum of the Maven distribution zip file $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum if ($distributionSha256Sum) { if ($USE_MVND) { Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." } Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." } } # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { Write-Error "fail to move MAVEN_HOME" } } finally { try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } } Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" ================================================ FILE: pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 3.4.0 com.ask.home video-stream 1.0.0-SNAPSHOT VideoStreaming Trying for Video Stream 23 1.18.34 org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-webflux org.projectlombok lombok ${lombok.version} org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine io.projectreactor reactor-test test org.apache.maven.plugins maven-compiler-plugin 3.13.0 ${java.version} ${java.version} org.projectlombok lombok ${lombok.version} org.springframework.boot spring-boot-maven-plugin video-stream ================================================ FILE: src/main/java/com/ask/home/videostream/Application.java ================================================ package com.ask.home.videostream; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ================================================ FILE: src/main/java/com/ask/home/videostream/adapter/ContentAdapter.java ================================================ package com.ask.home.videostream.adapter; import com.ask.home.videostream.model.Content; import com.ask.home.videostream.model.ContentRequest; import java.util.List; /** * ContentAdapter */ public interface ContentAdapter { Content getContent(ContentRequest contentRequest); Long getContentSize(ContentRequest contentRequest); List findAllContents(); Content findFileByKey(final String fileKey); } ================================================ FILE: src/main/java/com/ask/home/videostream/adapter/LocalFileContentAdapter.java ================================================ package com.ask.home.videostream.adapter; import com.ask.home.videostream.model.Content; import com.ask.home.videostream.model.ContentRequest; import com.ask.home.videostream.util.FileUtil; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; import static com.ask.home.videostream.util.FileUtil.getFilePath; /** * Extract the content from local to the device(volume/drive). */ @Slf4j public class LocalFileContentAdapter implements ContentAdapter { final public static String FILE_PATH_FORMAT = "%s/%s"; final private Map localFileMap; final private String localFilePath; /** * Constructor injection for the root content path. * * @param localFilePath String. */ public LocalFileContentAdapter(String localFilePath) { this.localFilePath = localFilePath; localFileMap = new HashMap<>(); } /** * find Object By key. * * @param fileKey String * @return Content. */ public Content findFileByKey(final String fileKey) { if (fileKey == null) { throw new RuntimeException("FileKey can't be null"); } if (localFileMap.isEmpty()) { findAllContents(); } return localFileMap.get(fileKey); } @Override public Content getContent(final ContentRequest contentRequest) { boolean isValid = validateRequest(contentRequest); if (!isValid) { throw new RuntimeException("Not a valid content request"); } try { byte[] content = readByBytesRange(contentRequest); return Content.builder().content(content).contentLength((long) content.length).rangeStart(contentRequest.getRangeStart()).rangeEnd(contentRequest.getRangeEnd()).build(); } catch (IOException e) { throw new RuntimeException(e); } } /** * read the bytes of the file by the range. * * @param contentRequest ContentRequest. * @return byte[] * @throws IOException ioException. */ private byte[] readByBytesRange(final ContentRequest contentRequest) throws IOException { Path path = Paths.get(getFilePath(localFilePath, contentRequest.getFilePath(), contentRequest.getFileName())); byte[] data = Files.readAllBytes(path); if (data.length == 0) { return data; } long end = contentRequest.getRangeEnd(); long start = contentRequest.getRangeStart(); byte[] result = new byte[(int) (end - start) + 1]; System.arraycopy(data, (int) start, result, 0, (int) (end - start) + 1); return result; } /** * Content length. * * @param contentRequest ContentRequest. * @return Long. */ @Override public Long getContentSize(ContentRequest contentRequest) { return Optional.ofNullable(contentRequest).map(_ -> Paths.get(getFilePath(localFilePath, contentRequest.getFilePath(), contentRequest.getFileName()))).map(this::sizeFromFile).orElse(0L); } @Override public List findAllContents() { Path path = Paths.get(new File(localFilePath).getAbsolutePath()); try (Stream stream = Files.walk(path, 10)) { return stream.filter(file -> !Files.isDirectory(file)).map(this::prepareContent).filter(Objects::nonNull).collect(Collectors.toList()); } catch (IOException e) { throw new RuntimeException(e); } } /** * prepareContent. * * @param path Path * @return Content */ private Content prepareContent(final Path path) { if (FileUtil.isVideoFile(path)) { final String fileName = path.getFileName().toString(); String extension = ""; int index = fileName.lastIndexOf('.'); if (index > 0) { extension = fileName.substring(index + 1); } BasicFileAttributes basicFileAttributes = getFileAttribute(path); // prepare content path - remove root file path String contentPath = path.getParent().toFile().toString(); int localFilePathIndex = contentPath.indexOf(localFilePath); if (localFilePathIndex > -1) { contentPath = contentPath.substring(localFilePathIndex); contentPath = contentPath.replace(localFilePath, ""); } Base64.Encoder encoder = Base64.getEncoder(); byte[] encodedByte = encoder.encode(basicFileAttributes.fileKey().toString().getBytes()); UUID uuid = UUID.nameUUIDFromBytes(encodedByte); Content content = Content.builder().contentName(fileName).objectKey(uuid.toString()).contentPath(contentPath).contentType(extension).totalContentSize(basicFileAttributes.size()).build(); localFileMap.put(uuid.toString(), content); return content; } return null; } /** * Read basic file Attributes * * @param path Path * @return BasicFileAttributes. */ private BasicFileAttributes getFileAttribute(final Path path) { try { return Files.readAttributes(path, BasicFileAttributes.class); } catch (IOException e) { throw new RuntimeException(e); } } /** * Getting the size from the path. * * @param path Path. * @return Long. */ private Long sizeFromFile(Path path) { try { return Files.size(path); } catch (IOException ioException) { log.error("Error while getting the file size", ioException); } return 0L; } /** * Validate the content Request. * * @param contentRequest ContentRequest. * @return boolean. */ private boolean validateRequest(final ContentRequest contentRequest) { if (contentRequest == null) { throw new RuntimeException("video request object is empty"); } if (contentRequest.getRangeStart() > contentRequest.getRangeEnd()) { return false; } return contentRequest.getFileName() != null && contentRequest.getFileType() != null; } } ================================================ FILE: src/main/java/com/ask/home/videostream/config/ApplicationConfig.java ================================================ package com.ask.home.videostream.config; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.WebFilter; @Configuration public class ApplicationConfig { /** * Method to enforce the context path webflux * @param serverProperties ServerProperties * @return WebFilter. */ @Bean public WebFilter contextPathWebFilter(ServerProperties serverProperties) { String contextPath = serverProperties.getServlet().getContextPath(); return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); if (request.getURI().getPath().startsWith(contextPath)) { return chain.filter( exchange.mutate() .request(request.mutate().contextPath(contextPath).build()) .build()); } return chain.filter(exchange); }; } } ================================================ FILE: src/main/java/com/ask/home/videostream/config/VideoStreamConfig.java ================================================ package com.ask.home.videostream.config; import com.ask.home.videostream.adapter.ContentAdapter; import com.ask.home.videostream.adapter.LocalFileContentAdapter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @Slf4j public class VideoStreamConfig { @Bean public ContentAdapter videoContentAdapter(@Value("${video.content.path}") final String videoContentRootPath) { log.info("video Content Path {}", videoContentRootPath); return new LocalFileContentAdapter(videoContentRootPath); } } ================================================ FILE: src/main/java/com/ask/home/videostream/constants/ApplicationConstants.java ================================================ package com.ask.home.videostream.constants; public class ApplicationConstants { public static final String VIDEO = "/video"; public static final String CONTENT_TYPE = "Content-Type"; public static final String CONTENT_LENGTH = "Content-Length"; public static final String VIDEO_CONTENT = "video/"; public static final String CONTENT_RANGE = "Content-Range"; public static final String ACCEPT_RANGES = "Accept-Ranges"; public static final String BYTES = "bytes"; public static final int CHUNK_SIZE = 314700; public static final int BYTE_RANGE = 1024; private ApplicationConstants() { } } ================================================ FILE: src/main/java/com/ask/home/videostream/controller/VideoController.java ================================================ package com.ask.home.videostream.controller; import com.ask.home.videostream.model.Content; import com.ask.home.videostream.service.VideoService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Mono; import java.util.List; @RestController @RequestMapping("/api/v1/videos") public class VideoController { private final VideoService videoService; public VideoController(VideoService videoService) { this.videoService = videoService; } @GetMapping("/stream/{fileType}/{filePathAndName}") public Mono> streamVideoByPath(@RequestHeader(value = "Range", required = false) String httpRangeList, @PathVariable("fileType") String fileType, @PathVariable("filePathAndName") String filePathAndName) { return Mono.just(videoService.prepareContentByFilePath(httpRangeList, filePathAndName, fileType)); } @GetMapping("/stream/object-key/{objectKey}") public Mono> streamVideoByObjectKey(@RequestHeader(value = "Range", required = false) String httpRangeList, @PathVariable("objectKey") String objectKey) { return Mono.just(videoService.prepareContentByObjectKey(httpRangeList, objectKey)); } @GetMapping public Mono>> getAllContents() { return Mono.just(videoService.getAllContents()); } } ================================================ FILE: src/main/java/com/ask/home/videostream/model/Content.java ================================================ package com.ask.home.videostream.model; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Builder; import lombok.Data; /** * Contains the values of extracted video content and metadata */ @Data @Builder @JsonInclude(JsonInclude.Include.NON_NULL) public class Content { private byte[] content; private Long rangeStart; private Long rangeEnd; private Long contentLength; private String contentPath; private String contentType; private String contentName; private Long totalContentSize; private String objectKey; } ================================================ FILE: src/main/java/com/ask/home/videostream/model/ContentRequest.java ================================================ package com.ask.home.videostream.model; import lombok.Builder; import lombok.Data; @Data @Builder public class ContentRequest { private String fileType; private String fileName; private long rangeStart; private long rangeEnd; private String filePath; } ================================================ FILE: src/main/java/com/ask/home/videostream/service/VideoService.java ================================================ package com.ask.home.videostream.service; import com.ask.home.videostream.adapter.ContentAdapter; import com.ask.home.videostream.model.Content; import com.ask.home.videostream.model.ContentRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import static com.ask.home.videostream.constants.ApplicationConstants.*; /** * Video Service that process the incoming request and extract the data out. */ @Service @Slf4j public class VideoService { private static final String CONTENT_RANGE_FORMAT = "%s %s-%s/%s"; private final ContentAdapter videoContentAdapter; public VideoService(final ContentAdapter videoContentAdapter) { this.videoContentAdapter = videoContentAdapter; } /** * Method to get the video data by the Object Key. * * @param range Range of the content size. * @param objectKey Object Key * @return byte array of video with ResponseEntity. */ public ResponseEntity prepareContentByObjectKey(final String range, final String objectKey) { final Content content = videoContentAdapter.findFileByKey(objectKey); if (content == null) { return ResponseEntity.notFound().build(); } final ContentRequest contentRequest = ContentRequest.builder().fileName(content.getContentName()).fileType(content.getContentType()).filePath(content.getContentPath()).build(); return prepareContent(range, contentRequest); } /** * Get the Content by the path * * @param range Range of the content size. * @param filePathAndName relative path of the file and file name * @param fileType File Type * @return byte array of video with ResponseEntity. */ public ResponseEntity prepareContentByFilePath(final String range, final String filePathAndName, final String fileType) { final String[] filePathAndNameSplit = filePathAndName.split("\\+"); final String fileName = filePathAndNameSplit[filePathAndNameSplit.length - 1]; final String filePath = Arrays.stream(filePathAndNameSplit).limit(filePathAndNameSplit.length - 1).collect(Collectors.joining("/")); final String fileNameAndType = String.format("%s.%s", fileName, fileType); final ContentRequest contentRequest = ContentRequest.builder().fileName(fileNameAndType).fileType(fileType).filePath(filePath).build(); return prepareContent(range, contentRequest); } /** * Get the content based on the request Object(ContentRequest) * * @param range Range of the content size. * @param contentRequest Content Data. * @return byte array of video with ResponseEntity. */ private ResponseEntity prepareContent(final String range, final ContentRequest contentRequest) { try { final Long fileSize = videoContentAdapter.getContentSize(contentRequest); if (fileSize < 1) { throw new RuntimeException("Not a valid file size"); } prepareContentRange(range, contentRequest); final Content content = videoContentAdapter.getContent(contentRequest); content.setContentType(contentRequest.getFileType()); content.setTotalContentSize(fileSize); return prepareResponseEntity(content); } catch (Exception exception) { log.error("Exception while reading the file {}", exception.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } /** * Prepare the response Entity * * @param content Content * @return ResponseEntity */ private ResponseEntity prepareResponseEntity(final Content content) { HttpStatus httpStatus = HttpStatus.PARTIAL_CONTENT; if (content.getRangeEnd() != null && content.getRangeEnd() >= content.getTotalContentSize()) { httpStatus = HttpStatus.OK; } return ResponseEntity.status(httpStatus).header(CONTENT_TYPE, VIDEO_CONTENT + content.getContentType()).header(ACCEPT_RANGES, BYTES).header(CONTENT_LENGTH, String.valueOf(content.getContentLength())).header(CONTENT_RANGE, String.format(CONTENT_RANGE_FORMAT, BYTES, content.getRangeStart(), content.getRangeEnd(), content.getTotalContentSize())).body(content.getContent()); } /** * Prepare the request * * @param range String. * @param contentRequest ContentRequest. */ private void prepareContentRange(final String range, final ContentRequest contentRequest) { // if range doesn't present default to chunk size. if (range == null) { contentRequest.setRangeStart(0L); contentRequest.setRangeEnd(CHUNK_SIZE); } else { //format Range: bytes=0-499 String[] ranges = range.split("-"); long rangeStart = Long.parseLong(ranges[0].substring(6)); // default rangeEnd with chunk size long rangeEnd = rangeStart + CHUNK_SIZE; // if range end present in the request then pick from there if (ranges.length > 1) { rangeEnd = Long.parseLong(ranges[1]); } // Get the minimum of file size or rangeEnd. rangeEnd = Math.min(rangeEnd, videoContentAdapter.getContentSize(contentRequest) - 1); contentRequest.setRangeStart(rangeStart); contentRequest.setRangeEnd(rangeEnd); } } /** * List Contents * * @return ResponseEntity> */ public ResponseEntity> getAllContents() { List contentList = videoContentAdapter.findAllContents(); if (contentList.isEmpty()) { return ResponseEntity.noContent().build(); } return ResponseEntity.ok(contentList); } } ================================================ FILE: src/main/java/com/ask/home/videostream/util/FileUtil.java ================================================ package com.ask.home.videostream.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import static com.ask.home.videostream.adapter.LocalFileContentAdapter.FILE_PATH_FORMAT; import static com.ask.home.videostream.constants.ApplicationConstants.VIDEO; public class FileUtil { private static final Logger log = LoggerFactory.getLogger(FileUtil.class); /** * Get the filePath. * * @return String. */ public static String getFilePath() { URL url = FileUtil.class.getResource(VIDEO); assert url != null; return new File(url.getFile()).getAbsolutePath(); } public static String getFilePath(final String basePath, String filePath, String fileName) { String path; if (filePath != null && filePath.trim().isEmpty()) { path = String.format(FILE_PATH_FORMAT, basePath, fileName); } else { path = String.format(FILE_PATH_FORMAT,String.format(FILE_PATH_FORMAT, basePath, filePath), fileName); } return new File(path).getAbsolutePath(); } /** * Check the file is video. * @param path Path * @return boolean */ public static boolean isVideoFile(Path path) { try { String contentType = Files.probeContentType(path); return contentType != null && contentType.startsWith("video/"); } catch (IOException ioException) { log.error("Exception in when checking the file is video file {}", ioException.getMessage()); return false; } } } ================================================ FILE: src/main/resources/application.yml ================================================ server: servlet: context-path: /video-service port: 8080 video: content: path: ${VIDEO_CONTENT_PATH:target/classes/video} ================================================ FILE: src/test/java/com/ask/home/videostream/ApplicationTests.java ================================================ package com.ask.home.videostream; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; @SpringBootTest class ApplicationTests { @Autowired ApplicationContext applicationContext; @Test void contextLoads() { Assertions.assertNotNull(applicationContext); } } ================================================ FILE: src/test/java/com/ask/home/videostream/adapter/LocalFileContentAdapterTest.java ================================================ package com.ask.home.videostream.adapter; import com.ask.home.videostream.model.Content; import com.ask.home.videostream.model.ContentRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import java.io.File; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @ExtendWith(MockitoExtension.class) class LocalFileContentAdapterTest { LocalFileContentAdapter localFileContentAdapter; @BeforeEach public void setup() { ClassLoader classLoader = getClass().getClassLoader(); File file = new File(classLoader.getResource("video").getFile()); localFileContentAdapter = new LocalFileContentAdapter(file.getAbsolutePath()); } @Test void findFileByKeyWithNullKey() { assertThrows(RuntimeException.class, () -> localFileContentAdapter.findFileByKey(null)); } @Test void findFileByKeyWithInvalidKey() { Content responseContent = localFileContentAdapter.findFileByKey("test"); assertNull(responseContent); } @Test void findFileByKeyWithValidKey() { Map cache = new HashMap<>(); String fileKey = "957e9073-9aec-3be2-a94e-268312e13bed"; Content content = Content.builder().contentName("test").objectKey(fileKey).build(); cache.put(fileKey, content); ReflectionTestUtils.setField(localFileContentAdapter, "localFileMap", cache); Content responseContent = localFileContentAdapter.findFileByKey(fileKey); assertNotNull(responseContent); assertNotNull(responseContent.getContentName()); assertNotNull(responseContent.getObjectKey()); assertEquals(fileKey, responseContent.getObjectKey()); } @Test void getContentWithEmptyData() { ContentRequest contentRequest = ContentRequest.builder().fileName("video_empty.mp4").fileType("mp4").filePath("").build(); Content content = localFileContentAdapter.getContent(contentRequest); assertNotNull(content); assertEquals(0, content.getContent().length); } @Test void getContentWithRealVideoFile() { ContentRequest contentRequest = ContentRequest.builder().fileName("toystory.mp4").fileType("mp4").filePath("").build(); Content content = localFileContentAdapter.getContent(contentRequest); assertNotNull(content); assertTrue(content.getContent().length > 0); } @Test void getContentWithNullObject() { assertThrows(RuntimeException.class, () -> localFileContentAdapter.getContent(null)); } @Test void getContentSizeWithValidFile() { ContentRequest contentRequest = ContentRequest.builder().fileName("toystory.mp4").fileType("mp4").filePath("").build(); long contentSize = localFileContentAdapter.getContentSize(contentRequest); assertEquals(33505479, contentSize); } @Test void findAllContents() { List contentList = localFileContentAdapter.findAllContents(); assertNotNull(contentList); assertFalse(contentList.isEmpty()); } } ================================================ FILE: src/test/java/com/ask/home/videostream/controller/VideoControllerTest.java ================================================ package com.ask.home.videostream.controller; import com.ask.home.videostream.service.VideoService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.reactive.server.WebTestClient; import java.util.Collections; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @WebFluxTest(controllers = VideoController.class) @ExtendWith(SpringExtension.class) class VideoControllerTest { @MockBean VideoService videoService; @Autowired private WebTestClient webTestClient; @Test void streamVideoWithFilePathAndName() { when(videoService.prepareContentByFilePath(any(), any(), any())).thenReturn(ResponseEntity.ok(new byte[]{})); webTestClient.get().uri("/api/v1/videos/stream/mp4/toystory").exchange().expectStatus().is2xxSuccessful(); verify(videoService, times(1)).prepareContentByFilePath(any(), any(), any()); verify(videoService, times(0)).prepareContentByObjectKey(any(), any()); } @Test void streamVideoWithFilePathFolderAndName() { when(videoService.prepareContentByFilePath(any(), any(), any())).thenReturn(ResponseEntity.ok(new byte[]{})); webTestClient.get().uri("/api/v1/videos/stream/mp4/video1+toystory").exchange().expectStatus().is2xxSuccessful(); verify(videoService, times(1)).prepareContentByFilePath(any(), any(), any()); verify(videoService, times(0)).prepareContentByObjectKey(any(), any()); } @Test void testStreamVideoWithObjectKey() { when(videoService.prepareContentByObjectKey(any(), any())).thenReturn(ResponseEntity.ok(new byte[]{})); webTestClient.get().uri("/api/v1/videos/stream/object-key/test-key").exchange().expectStatus().is2xxSuccessful(); verify(videoService, times(0)).prepareContentByFilePath(any(), any(), any()); verify(videoService, times(1)).prepareContentByObjectKey(any(), any()); } @Test void getAllContents() { when(videoService.getAllContents()).thenReturn(ResponseEntity.ok(Collections.emptyList())); webTestClient.get().uri("/api/v1/videos").exchange().expectStatus().is2xxSuccessful(); verify(videoService, times(0)).prepareContentByFilePath(any(), any(), any()); verify(videoService, times(0)).prepareContentByObjectKey(any(), any()); verify(videoService, times(1)).getAllContents(); } } ================================================ FILE: src/test/java/com/ask/home/videostream/service/VideoServiceTest.java ================================================ package com.ask.home.videostream.service; import com.ask.home.videostream.adapter.ContentAdapter; import com.ask.home.videostream.model.Content; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.ResponseEntity; import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; @ExtendWith(MockitoExtension.class) class VideoServiceTest { @InjectMocks VideoService videoService; @Mock ContentAdapter videoContentAdapter; @Test void prepareContentByObjectKeyWithValidObjectKey() { Content content = Content.builder().contentPath("").contentName("toystory.mp4").content(new byte[]{}).build(); Mockito.when(videoContentAdapter.getContent(any())).thenReturn(content); Mockito.when(videoContentAdapter.findFileByKey(any())).thenReturn(content); Mockito.when(videoContentAdapter.getContentSize(any())).thenReturn(10L); ResponseEntity responseEntity = videoService.prepareContentByObjectKey("bytes=0-", "test-key"); assertNotNull(responseEntity); assertTrue(responseEntity.getStatusCode().is2xxSuccessful()); } @Test void prepareContentByObjectKeyWithContentNotFound() { Mockito.when(videoContentAdapter.findFileByKey(any())).thenReturn(null); ResponseEntity responseEntity = videoService.prepareContentByObjectKey("bytes=0-", "test-key"); assertNotNull(responseEntity); assertTrue(responseEntity.getStatusCode().is4xxClientError()); } @Test void prepareContentByFilePath() { Content content = Content.builder().contentPath("").contentName("toystory.mp4").content(new byte[]{}).build(); Mockito.when(videoContentAdapter.getContent(any())).thenReturn(content); Mockito.when(videoContentAdapter.getContentSize(any())).thenReturn(10L); ResponseEntity responseEntity = videoService.prepareContentByFilePath("bytes=0-", "toystory", "mp4"); assertNotNull(responseEntity); assertTrue(responseEntity.getStatusCode().is2xxSuccessful()); } @Test void prepareContentByFilePathWithoutRange() { Content content = Content.builder().contentPath("").contentName("toystory.mp4").content(new byte[]{}).build(); Mockito.when(videoContentAdapter.getContent(any())).thenReturn(content); Mockito.when(videoContentAdapter.getContentSize(any())).thenReturn(10L); ResponseEntity responseEntity = videoService.prepareContentByFilePath(null, "toystory", "mp4"); assertNotNull(responseEntity); assertTrue(responseEntity.getStatusCode().is2xxSuccessful()); } @Test void getAllContentsWithData() { Content content = Content.builder().contentPath("").contentName("toystory.mp4").content(new byte[]{}).build(); Mockito.when(videoContentAdapter.findAllContents()).thenReturn(Collections.singletonList(content)); ResponseEntity> responseEntity = videoService.getAllContents(); assertNotNull(responseEntity); assertTrue(responseEntity.getStatusCode().is2xxSuccessful()); } @Test void getAllContentsWithNoData() { Mockito.when(videoContentAdapter.findAllContents()).thenReturn(Collections.emptyList()); ResponseEntity> responseEntity = videoService.getAllContents(); assertNotNull(responseEntity); assertTrue(responseEntity.getStatusCode().is2xxSuccessful()); assertEquals(204, responseEntity.getStatusCode().value()); } } ================================================ FILE: src/test/resources/application-test.yml ================================================ server: servlet: context-path: /video-service port: 8080 video: content: path: ${VIDEO_CONTENT_PATH:target/classes/video}