[Feature] Preprocessing and dev QoL changes #1

Merged
bivashy merged 4 commits from feature/preprocessing into main 2025-12-24 18:48:02 +00:00
21 changed files with 645 additions and 44 deletions
Showing only changes of commit 0230cae852 - Show all commits

View File

@ -1,18 +1,37 @@
package com.backend.hls.proxy.config; package com.backend.hls.proxy.config;
import java.util.concurrent.TimeUnit;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager; import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import com.backend.hls.proxy.model.SimpleResponse;
import com.github.benmanes.caffeine.cache.Caffeine;
@Configuration @Configuration
@EnableCaching @EnableCaching
public class CacheConfig { public class CacheConfig {
@Bean @Bean
public CacheManager cacheManager() { public CacheManager cacheManager() {
return new CaffeineCacheManager("hlsPlaylistContent", "playlistSegmentContent"); CaffeineCacheManager cacheManager = new CaffeineCacheManager("hlsPlaylistContent", "playlistSegmentContent");
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.weigher((Object key, Object value) -> {
if (value instanceof byte[] valueBytes) {
return valueBytes.length;
}
if (value instanceof SimpleResponse<?> response && response.getBody() instanceof byte[] body) {
return body.length;
}
return 0;
})
.maximumWeight(500 * 1024 * 1024)
.recordStats());
return cacheManager;
} }
} }

View File

@ -0,0 +1,16 @@
package com.backend.hls.proxy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.backend.hls.proxy.service.NoopPreprocessService;
import com.backend.hls.proxy.service.PreprocessService;
@Configuration
public class ProcessConfig {
@Bean
public PreprocessService preprocessService() {
return new NoopPreprocessService();
}
}

View File

@ -6,6 +6,8 @@ import java.util.Optional;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -93,7 +95,6 @@ public class ProxyServeController {
RangeRequest range = RangeRequest.parse(rangeHeader, contentLength); RangeRequest range = RangeRequest.parse(rangeHeader, contentLength);
if (range == null) { if (range == null) {
// Invalid range, return 416 Range Not Satisfiable
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.add("Content-Range", "bytes */" + contentLength); headers.add("Content-Range", "bytes */" + contentLength);
return new ResponseEntity<>(headers, HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE); return new ResponseEntity<>(headers, HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);

View File

@ -0,0 +1,13 @@
package com.backend.hls.proxy.service;
import org.springframework.stereotype.Service;
@Service
public class NoopPreprocessService implements PreprocessService {
@Override
public byte[] preprocess(byte[] data) {
return data;
}
}

View File

@ -1,67 +1,5 @@
package com.backend.hls.proxy.service; package com.backend.hls.proxy.service;
import java.io.ByteArrayInputStream; public interface PreprocessService {
import java.io.IOException; byte[] preprocess(byte[] data);
import java.io.InputStream;
import java.util.Random;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import com.backend.hls.proxy.util.PipeUtil;
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
@Service
public class PreprocessService {
private static final Logger logger = LoggerFactory.getLogger(PreprocessService.class);
public byte[] preprocess(byte[] data) {
try {
String format = findFormat(data);
logger.info("format is {}", format);
return randomEffectsHLS(data, format, "/usr/bin/ffmpeg");
} catch (IOException e) {
e.printStackTrace();
return data;
}
}
public static byte[] randomEffectsHLS(byte[] data, String inputFormat, String ffmpegPath) throws IOException {
try (InputStream inputStream = new ByteArrayInputStream(data)) {
String[] effects = {
"hue=s=10", // Color shift
"edgedetect=mode=colormix", // Edge detection
"boxblur=10:1", // Heavy blur
"noise=alls=20:allf=t", // Film grain noise
"colorchannelmixer=.3:.4:.3:0:.3:.4:.3:0:.3:.4:.3", // Vintage
"rotate=0.1*c", // Slight rotation
"scale=iw/2:ih/2" // Pixelate
};
Random random = new Random();
String randomEffect = effects[random.nextInt(effects.length)];
logger.info("applied effect {}", randomEffect);
String[] ffmpegArgs = {
"-vf", randomEffect,
"-f", inputFormat,
};
return PipeUtil.executeWithPipe(ffmpegPath, inputStream, inputFormat, ffmpegArgs);
}
}
private String findFormat(byte[] data) throws IOException {
FFmpegProbeResult result = PipeUtil.probeWithPipe("/usr/bin/ffprobe", new ByteArrayInputStream(data));
logger.info("info: {}", result.streams.stream().map(stream -> stream.codec_type).collect(Collectors.toList()));
if (result.streams.stream().noneMatch(stream -> stream.codec_type.name().equals("VIDEO"))) {
throw new IOException("No video stream found");
}
return result.format.format_name;
}
} }

View File

@ -0,0 +1,67 @@
package com.backend.hls.proxy.service;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Random;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import com.backend.hls.proxy.util.PipeUtil;
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
@Service
public class RandomEffectPreprocessService implements PreprocessService {
private static final Logger logger = LoggerFactory.getLogger(PreprocessService.class);
public byte[] preprocess(byte[] data) {
try {
String format = findFormat(data);
logger.info("format is {}", format);
return randomEffectsHLS(data, format, "/usr/bin/ffmpeg");
} catch (IOException e) {
e.printStackTrace();
return data;
}
}
public static byte[] randomEffectsHLS(byte[] data, String inputFormat, String ffmpegPath) throws IOException {
try (InputStream inputStream = new ByteArrayInputStream(data)) {
String[] effects = {
"hue=s=10", // Color shift
"edgedetect=mode=colormix", // Edge detection
"boxblur=10:1", // Heavy blur
"noise=alls=20:allf=t", // Film grain noise
"colorchannelmixer=.3:.4:.3:0:.3:.4:.3:0:.3:.4:.3", // Vintage
"rotate=0.1*c", // Slight rotation
"scale=iw/2:ih/2" // Pixelate
};
Random random = new Random();
String randomEffect = effects[random.nextInt(effects.length)];
logger.info("applied effect {}", randomEffect);
String[] ffmpegArgs = {
"-vf", randomEffect,
"-f", inputFormat,
};
return PipeUtil.executeWithPipe(ffmpegPath, inputStream, inputFormat, ffmpegArgs);
}
}
private String findFormat(byte[] data) throws IOException {
FFmpegProbeResult result = PipeUtil.probeWithPipe("/usr/bin/ffprobe", new ByteArrayInputStream(data));
logger.info("info: {}", result.streams.stream().map(stream -> stream.codec_type).collect(Collectors.toList()));
if (result.streams.stream().noneMatch(stream -> stream.codec_type.name().equals("VIDEO"))) {
throw new IOException("No video stream found");
}
return result.format.format_name;
}
}