mirror of https://github.com/usbharu/Hideout.git
feat: メディアアップロード処理を追加
This commit is contained in:
parent
a93131b5dc
commit
43ef619eee
|
@ -24,12 +24,13 @@ import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
|
||||||
import dev.usbharu.hideout.core.external.media.MediaProcessor
|
import dev.usbharu.hideout.core.external.media.MediaProcessor
|
||||||
import dev.usbharu.hideout.core.external.mediastore.MediaStore
|
import dev.usbharu.hideout.core.external.mediastore.MediaStore
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import dev.usbharu.hideout.core.domain.model.media.Media as MediaModel
|
import dev.usbharu.hideout.core.domain.model.media.Media as MediaModel
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class UploadMediaApplicationService(
|
class UploadMediaApplicationService(
|
||||||
private val mediaProcessor: MediaProcessor,
|
@Qualifier("delegate") private val mediaProcessor: MediaProcessor,
|
||||||
private val mediaStore: MediaStore,
|
private val mediaStore: MediaStore,
|
||||||
private val mediaRepository: MediaRepository,
|
private val mediaRepository: MediaRepository,
|
||||||
private val idGenerateService: IdGenerateService,
|
private val idGenerateService: IdGenerateService,
|
||||||
|
@ -42,7 +43,7 @@ class UploadMediaApplicationService(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun internalExecute(command: UploadMedia, executor: CommandExecutor): Media {
|
override suspend fun internalExecute(command: UploadMedia, executor: CommandExecutor): Media {
|
||||||
val process = mediaProcessor.process(command.path)
|
val process = mediaProcessor.process(command.path, command.name, null)
|
||||||
val id = idGenerateService.generateId()
|
val id = idGenerateService.generateId()
|
||||||
val thumbnailUri = if (process.thumbnailPath != null) {
|
val thumbnailUri = if (process.thumbnailPath != null) {
|
||||||
mediaStore.upload(process.thumbnailPath, "thumbnail-$id")
|
mediaStore.upload(process.thumbnailPath, "thumbnail-$id")
|
||||||
|
@ -59,7 +60,7 @@ class UploadMediaApplicationService(
|
||||||
thumbnailUri,
|
thumbnailUri,
|
||||||
process.fileType,
|
process.fileType,
|
||||||
process.mimeType,
|
process.mimeType,
|
||||||
MediaBlurHash(process.blurHash),
|
process.blurHash?.let { MediaBlurHash(it) },
|
||||||
command.description?.let { MediaDescription(it) }
|
command.description?.let { MediaDescription(it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package dev.usbharu.hideout.core.config
|
||||||
|
|
||||||
|
import org.bytedeco.ffmpeg.global.avcodec
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
|
||||||
|
@ConfigurationProperties("hideout.media.video.ffmpeg")
|
||||||
|
data class FFmpegVideoConfig(
|
||||||
|
val frameRate: Int = 60,
|
||||||
|
val maxWidth: Int = 1920,
|
||||||
|
val maxHeight: Int = 1080,
|
||||||
|
val format: String = "mp4",
|
||||||
|
val videoCodec: Int = avcodec.AV_CODEC_ID_H264,
|
||||||
|
val audioCodec: Int = avcodec.AV_CODEC_ID_AAC,
|
||||||
|
val videoQuality: Double = 1.0,
|
||||||
|
val videoOption: List<String> = listOf("preset", "ultrafast"),
|
||||||
|
val maxBitrate: Int = 1300000,
|
||||||
|
)
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.usbharu.hideout.core.config
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
|
||||||
|
@ConfigurationProperties("hideout.media.image.imageio")
|
||||||
|
data class ImageIOImageConfig(
|
||||||
|
val thumbnailsWidth: Int = 1000,
|
||||||
|
val thumbnailsHeight: Int = 1000,
|
||||||
|
val format: String = "jpeg"
|
||||||
|
)
|
|
@ -0,0 +1,17 @@
|
||||||
|
package dev.usbharu.hideout.core.config
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
|
||||||
|
/**
|
||||||
|
* メディアの保存にローカルファイルシステムを使用する際のコンフィグ
|
||||||
|
*
|
||||||
|
* @property path フォゾンする場所へのパス。 /から始めると絶対パスとなります。
|
||||||
|
* @property publicUrl 公開用URL 省略可能 指定するとHideoutがファイルを配信しなくなります。
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties("hideout.storage.local")
|
||||||
|
@ConditionalOnProperty("hideout.storage.type", havingValue = "local", matchIfMissing = true)
|
||||||
|
data class LocalStorageConfig(
|
||||||
|
val path: String = "files",
|
||||||
|
val publicUrl: String?
|
||||||
|
)
|
|
@ -0,0 +1,34 @@
|
||||||
|
package dev.usbharu.hideout.core.config
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
|
||||||
|
import software.amazon.awssdk.regions.Region
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
@ConfigurationProperties("hideout.storage.s3")
|
||||||
|
@ConditionalOnProperty("hideout.storage.type", havingValue = "s3")
|
||||||
|
data class S3StorageConfig(
|
||||||
|
val endpoint: String,
|
||||||
|
val publicUrl: String,
|
||||||
|
val bucket: String,
|
||||||
|
val region: String,
|
||||||
|
val accessKey: String,
|
||||||
|
val secretKey: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class AwsConfig {
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty("hideout.storage.type", havingValue = "s3")
|
||||||
|
fun s3Client(awsConfig: S3StorageConfig): S3Client {
|
||||||
|
return S3Client.builder()
|
||||||
|
.endpointOverride(URI.create(awsConfig.endpoint))
|
||||||
|
.region(Region.of(awsConfig.region))
|
||||||
|
.credentialsProvider { AwsBasicCredentials.create(awsConfig.accessKey, awsConfig.secretKey) }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
22
hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/DelegateMediaProcessor.kt
vendored
Normal file
22
hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/DelegateMediaProcessor.kt
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package dev.usbharu.hideout.core.external.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.media.MimeType
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Qualifier("delegate")
|
||||||
|
class DelegateMediaProcessor(
|
||||||
|
private val fileTypeDeterminer: FileTypeDeterminer,
|
||||||
|
private val mediaProcessors: List<MediaProcessor>
|
||||||
|
) : MediaProcessor {
|
||||||
|
override fun isSupported(mimeType: MimeType): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun process(path: Path, filename: String, mimeType: MimeType?): ProcessedMedia {
|
||||||
|
val fileType = fileTypeDeterminer.fileType(path, filename)
|
||||||
|
return mediaProcessors.first { it.isSupported(fileType) }.process(path, filename, fileType)
|
||||||
|
}
|
||||||
|
}
|
8
hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/FileTypeDeterminator.kt
vendored
Normal file
8
hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/FileTypeDeterminator.kt
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package dev.usbharu.hideout.core.external.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.media.MimeType
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
interface FileTypeDeterminer {
|
||||||
|
fun fileType(path: Path, filename: String): MimeType
|
||||||
|
}
|
|
@ -16,8 +16,10 @@
|
||||||
|
|
||||||
package dev.usbharu.hideout.core.external.media
|
package dev.usbharu.hideout.core.external.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.media.MimeType
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|
||||||
interface MediaProcessor {
|
interface MediaProcessor {
|
||||||
suspend fun process(path: Path): ProcessedMedia
|
fun isSupported(mimeType: MimeType): Boolean
|
||||||
|
suspend fun process(path: Path, filename: String, mimeType: MimeType?): ProcessedMedia
|
||||||
}
|
}
|
|
@ -25,5 +25,5 @@ data class ProcessedMedia(
|
||||||
val thumbnailPath: Path?,
|
val thumbnailPath: Path?,
|
||||||
val fileType: FileType,
|
val fileType: FileType,
|
||||||
val mimeType: MimeType,
|
val mimeType: MimeType,
|
||||||
val blurHash: String,
|
val blurHash: String?,
|
||||||
)
|
)
|
||||||
|
|
51
hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/TikaFileTypeDeterminer.kt
vendored
Normal file
51
hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/media/TikaFileTypeDeterminer.kt
vendored
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package dev.usbharu.hideout.core.external.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.domain.model.media.FileType
|
||||||
|
import dev.usbharu.hideout.core.domain.model.media.MimeType
|
||||||
|
import org.apache.tika.Tika
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class TikaFileTypeDeterminer : FileTypeDeterminer {
|
||||||
|
override fun fileType(path: Path, filename: String): MimeType {
|
||||||
|
logger.info("START Detect file type name: {}", filename)
|
||||||
|
|
||||||
|
val tika = Tika()
|
||||||
|
|
||||||
|
val detect = try {
|
||||||
|
tika.detect(path)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
logger.warn("FAILED Detect file type", e)
|
||||||
|
"application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
val type = detect.substringBefore("/")
|
||||||
|
val fileType = when (type) {
|
||||||
|
"image" -> {
|
||||||
|
FileType.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
"video" -> {
|
||||||
|
FileType.Video
|
||||||
|
}
|
||||||
|
|
||||||
|
"audio" -> {
|
||||||
|
FileType.Audio
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
FileType.Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val mimeType = MimeType(type, detect.substringAfter("/"), fileType)
|
||||||
|
|
||||||
|
logger.info("SUCCESS Detect file type name: {},MimeType: {}", filename, mimeType)
|
||||||
|
return mimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(TikaFileTypeDeterminer::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package dev.usbharu.hideout.core.infrastructure.awss3
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.config.S3StorageConfig
|
||||||
|
import dev.usbharu.hideout.core.external.mediastore.MediaStore
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import software.amazon.awssdk.core.sync.RequestBody
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client
|
||||||
|
import software.amazon.awssdk.services.s3.model.PutObjectRequest
|
||||||
|
import java.net.URI
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@ConditionalOnProperty("hideout.storage.type", havingValue = "s3")
|
||||||
|
class AWSS3MediaStore(
|
||||||
|
private val s3StorageConfig: S3StorageConfig,
|
||||||
|
private val s3Client: S3Client
|
||||||
|
) : MediaStore {
|
||||||
|
override suspend fun upload(path: Path, id: String): URI {
|
||||||
|
logger.info("MEDIA upload. {}", id)
|
||||||
|
|
||||||
|
val fileUploadRequest = PutObjectRequest.builder()
|
||||||
|
.bucket(s3StorageConfig.bucket)
|
||||||
|
.key(id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
logger.info("MEDIA upload. bucket: {} key: {}", s3StorageConfig.bucket, id)
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
s3Client.putObject(fileUploadRequest, RequestBody.fromFile(path))
|
||||||
|
}
|
||||||
|
val successSavedMedia = URI.create("${s3StorageConfig.publicUrl}/${s3StorageConfig.bucket}/${id}")
|
||||||
|
|
||||||
|
|
||||||
|
logger.info("SUCCESS Media upload. {}", id)
|
||||||
|
logger.debug(
|
||||||
|
"name: {} url: {}",
|
||||||
|
id,
|
||||||
|
successSavedMedia,
|
||||||
|
)
|
||||||
|
|
||||||
|
return successSavedMedia
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(AWSS3MediaStore::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package dev.usbharu.hideout.core.infrastructure.localfilesystem
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.config.ApplicationConfig
|
||||||
|
import dev.usbharu.hideout.core.config.LocalStorageConfig
|
||||||
|
import dev.usbharu.hideout.core.external.mediastore.MediaStore
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.net.URI
|
||||||
|
import java.nio.file.Path
|
||||||
|
import kotlin.io.path.copyTo
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@ConditionalOnProperty("hideout.storage.type", havingValue = "local", matchIfMissing = true)
|
||||||
|
class LocalFileSystemMediaStore(
|
||||||
|
localStorageConfig: LocalStorageConfig,
|
||||||
|
applicationConfig: ApplicationConfig
|
||||||
|
) :
|
||||||
|
MediaStore {
|
||||||
|
|
||||||
|
private val publicUrl = localStorageConfig.publicUrl ?: "${applicationConfig.url}/files/"
|
||||||
|
override suspend fun upload(path: Path, id: String): URI {
|
||||||
|
logger.info("START Media upload. {}", id)
|
||||||
|
val fileSavePath = buildSavePath(path, id)
|
||||||
|
|
||||||
|
val fileSavePathString = fileSavePath.toAbsolutePath().toString()
|
||||||
|
logger.info("MEDIA save. path: {}", fileSavePathString)
|
||||||
|
|
||||||
|
@Suppress("TooGenericExceptionCaught") try {
|
||||||
|
path.copyTo(fileSavePath)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn("FAILED to Save the media.", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("SUCCESS Media upload. {}", id)
|
||||||
|
return URI.create(publicUrl).resolve(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSavePath(savePath: Path, name: String): Path = savePath.resolve(name)
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(LocalFileSystemMediaStore::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dev.usbharu.hideout.core.infrastructure.media.common
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
|
||||||
|
interface GenerateBlurhash {
|
||||||
|
fun generateBlurhash(bufferedImage: BufferedImage): String
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package dev.usbharu.hideout.core.infrastructure.media.common
|
||||||
|
|
||||||
|
import io.trbl.blurhash.BlurHash
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class GenerateBlurhashImpl : GenerateBlurhash {
|
||||||
|
override fun generateBlurhash(bufferedImage: BufferedImage): String {
|
||||||
|
return BlurHash.encode(bufferedImage)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package dev.usbharu.hideout.core.infrastructure.media.image
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.config.ImageIOImageConfig
|
||||||
|
import dev.usbharu.hideout.core.domain.model.media.FileType
|
||||||
|
import dev.usbharu.hideout.core.domain.model.media.MimeType
|
||||||
|
import dev.usbharu.hideout.core.external.media.MediaProcessor
|
||||||
|
import dev.usbharu.hideout.core.external.media.ProcessedMedia
|
||||||
|
import dev.usbharu.hideout.core.infrastructure.media.common.GenerateBlurhash
|
||||||
|
import net.coobird.thumbnailator.Thumbnails
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.awt.Color
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.*
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
import kotlin.io.path.inputStream
|
||||||
|
import kotlin.io.path.outputStream
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Qualifier("image")
|
||||||
|
class ImageIOImageProcessor(
|
||||||
|
private val imageIOImageConfig: ImageIOImageConfig,
|
||||||
|
private val blurhash: GenerateBlurhash
|
||||||
|
) : MediaProcessor {
|
||||||
|
override fun isSupported(mimeType: MimeType): Boolean {
|
||||||
|
return mimeType.fileType == FileType.Image || mimeType.type == "image"
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun process(path: Path, filename: String, mimeType: MimeType?): ProcessedMedia {
|
||||||
|
val read = ImageIO.read(path.inputStream())
|
||||||
|
|
||||||
|
val bufferedImage = BufferedImage(read.width, read.height, BufferedImage.TYPE_INT_RGB)
|
||||||
|
|
||||||
|
val graphics = bufferedImage.createGraphics()
|
||||||
|
|
||||||
|
graphics.drawImage(read, 0, 0, Color.BLACK, null)
|
||||||
|
|
||||||
|
val tempFileName = UUID.randomUUID().toString()
|
||||||
|
val tempFile = Files.createTempFile(tempFileName, "tmp")
|
||||||
|
|
||||||
|
val thumbnailPath = run {
|
||||||
|
val tempThumbnailFile = Files.createTempFile("thumbnail-$tempFileName", ".tmp")
|
||||||
|
|
||||||
|
tempThumbnailFile.outputStream().use {
|
||||||
|
val write = ImageIO.write(
|
||||||
|
Thumbnails.of(bufferedImage)
|
||||||
|
.size(imageIOImageConfig.thumbnailsWidth, imageIOImageConfig.thumbnailsHeight)
|
||||||
|
.imageType(BufferedImage.TYPE_INT_RGB)
|
||||||
|
.asBufferedImage(),
|
||||||
|
imageIOImageConfig.format,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
tempThumbnailFile.takeIf { write }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFile.outputStream().use {
|
||||||
|
if (ImageIO.write(bufferedImage, imageIOImageConfig.format, it).not()) {
|
||||||
|
logger.warn("Failed to save a temporary file. type: {} ,path: {}", imageIOImageConfig.format, tempFile)
|
||||||
|
throw Exception("Failed to save a temporary file.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProcessedMedia(
|
||||||
|
tempFile,
|
||||||
|
thumbnailPath,
|
||||||
|
FileType.Image,
|
||||||
|
MimeType("image", imageIOImageConfig.format, FileType.Image),
|
||||||
|
blurhash.generateBlurhash(bufferedImage)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(ImageIOImageProcessor::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package dev.usbharu.hideout.core.infrastructure.media.video
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.config.FFmpegVideoConfig
|
||||||
|
import dev.usbharu.hideout.core.domain.model.media.FileType
|
||||||
|
import dev.usbharu.hideout.core.domain.model.media.MimeType
|
||||||
|
import dev.usbharu.hideout.core.external.media.MediaProcessor
|
||||||
|
import dev.usbharu.hideout.core.external.media.ProcessedMedia
|
||||||
|
import dev.usbharu.hideout.core.infrastructure.media.common.GenerateBlurhash
|
||||||
|
import org.bytedeco.javacv.FFmpegFrameFilter
|
||||||
|
import org.bytedeco.javacv.FFmpegFrameGrabber
|
||||||
|
import org.bytedeco.javacv.FFmpegFrameRecorder
|
||||||
|
import org.bytedeco.javacv.Java2DFrameConverter
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Qualifier("video")
|
||||||
|
class FFmpegVideoProcessor(
|
||||||
|
private val fFmpegVideoConfig: FFmpegVideoConfig,
|
||||||
|
private val generateBlurhash: GenerateBlurhash
|
||||||
|
) : MediaProcessor {
|
||||||
|
override fun isSupported(mimeType: MimeType): Boolean {
|
||||||
|
return mimeType.fileType == FileType.Video || mimeType.type == "video"
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun process(path: Path, filename: String, mimeType: MimeType?): ProcessedMedia {
|
||||||
|
val tempFile = Files.createTempFile("hideout-movie-processor-", ".tmp")
|
||||||
|
val thumbnailFile = Files.createTempFile("hideout-movie-thumbnail-generate-", ".tmp")
|
||||||
|
logger.info("START Convert Movie Media {}", filename)
|
||||||
|
var bufferedImage: BufferedImage? = null
|
||||||
|
FFmpegFrameGrabber(path.toFile()).use { grabber ->
|
||||||
|
grabber.start()
|
||||||
|
val width = min(fFmpegVideoConfig.maxWidth, grabber.imageWidth)
|
||||||
|
val height = min(fFmpegVideoConfig.maxHeight, grabber.imageHeight)
|
||||||
|
val frameRate = fFmpegVideoConfig.frameRate
|
||||||
|
|
||||||
|
logger.debug("Movie Media Width {}, Height {}", width, height)
|
||||||
|
|
||||||
|
FFmpegFrameFilter(
|
||||||
|
"fps=fps=$frameRate",
|
||||||
|
"anull",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
grabber.audioChannels
|
||||||
|
).use { filter ->
|
||||||
|
|
||||||
|
filter.sampleFormat = grabber.sampleFormat
|
||||||
|
filter.sampleRate = grabber.sampleRate
|
||||||
|
filter.pixelFormat = grabber.pixelFormat
|
||||||
|
filter.frameRate = grabber.frameRate
|
||||||
|
filter.start()
|
||||||
|
|
||||||
|
val videoBitRate = min(fFmpegVideoConfig.maxBitrate, (width * height * frameRate * 1 * 0.07).toInt())
|
||||||
|
|
||||||
|
logger.debug("Movie Media BitRate {}", videoBitRate)
|
||||||
|
|
||||||
|
FFmpegFrameRecorder(tempFile.toFile(), width, height, grabber.audioChannels).use {
|
||||||
|
it.sampleRate = grabber.sampleRate
|
||||||
|
it.format = fFmpegVideoConfig.format
|
||||||
|
it.videoCodec = fFmpegVideoConfig.videoCodec
|
||||||
|
it.audioCodec = fFmpegVideoConfig.audioCodec
|
||||||
|
it.audioChannels = grabber.audioChannels
|
||||||
|
it.videoQuality = fFmpegVideoConfig.videoQuality
|
||||||
|
it.frameRate = frameRate.toDouble()
|
||||||
|
it.setVideoOption("preset", "ultrafast")
|
||||||
|
it.timestamp = 0
|
||||||
|
it.gopSize = frameRate
|
||||||
|
it.videoBitrate = videoBitRate
|
||||||
|
it.start()
|
||||||
|
|
||||||
|
|
||||||
|
val frameConverter = Java2DFrameConverter()
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val grab = grabber.grab() ?: break
|
||||||
|
|
||||||
|
if (bufferedImage == null) {
|
||||||
|
bufferedImage = frameConverter.convert(grab)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grab.image != null || grab.samples != null) {
|
||||||
|
filter.push(grab)
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
val frame = filter.pull() ?: break
|
||||||
|
it.record(frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferedImage != null) {
|
||||||
|
ImageIO.write(bufferedImage, "jpeg", thumbnailFile.toFile())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("SUCCESS Convert Movie Media {}", filename)
|
||||||
|
|
||||||
|
return ProcessedMedia(
|
||||||
|
tempFile,
|
||||||
|
thumbnailFile,
|
||||||
|
FileType.Video,
|
||||||
|
MimeType("video", fFmpegVideoConfig.format, FileType.Video),
|
||||||
|
bufferedImage?.let { generateBlurhash.generateBlurhash(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(FFmpegVideoProcessor::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -215,7 +215,7 @@ private fun toStatus(it: ResultRow) = Status(
|
||||||
followingCount = it[Actors.followingCount],
|
followingCount = it[Actors.followingCount],
|
||||||
noindex = false,
|
noindex = false,
|
||||||
moved = false,
|
moved = false,
|
||||||
suspendex = false,
|
suspended = false,
|
||||||
limited = false
|
limited = false
|
||||||
),
|
),
|
||||||
content = it[Posts.text],
|
content = it[Posts.text],
|
||||||
|
|
Loading…
Reference in New Issue