Skip to content

Commit

Permalink
Merge pull request #147 from Javernaut/feature/new_protocols_support
Browse files Browse the repository at this point in the history
New protocols support
  • Loading branch information
Javernaut authored Dec 14, 2024
2 parents f159b1d + 6b492d8 commit 7c3de1b
Show file tree
Hide file tree
Showing 28 changed files with 446 additions and 342 deletions.
1 change: 0 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,5 @@ dependencies {
androidTestImplementation(libs.androidx.test.uiautomator)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.ext.truth)
androidTestImplementation(libs.mockk.instrumentation)
androidTestImplementation(libs.screengrab)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.javernaut.whatthecodec.screenshots

import android.graphics.BitmapFactory
import android.os.Build
import android.util.Size
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
Expand All @@ -10,28 +11,33 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.javernaut.whatthecodec.compose.theme.WhatTheCodecTheme
import com.javernaut.whatthecodec.feature.settings.api.content.AudioStreamFeature
import com.javernaut.whatthecodec.feature.settings.api.content.ContentSettingsRepository
import com.javernaut.whatthecodec.feature.settings.api.content.SubtitleStreamFeature
import com.javernaut.whatthecodec.feature.settings.api.content.VideoStreamFeature
import com.javernaut.whatthecodec.feature.settings.api.theme.AppTheme
import com.javernaut.whatthecodec.feature.settings.api.theme.ThemeSettings
import com.javernaut.whatthecodec.feature.settings.api.theme.ThemeSettingsRepository
import com.javernaut.whatthecodec.feature.settings.data.content.completeEnumSet
import com.javernaut.whatthecodec.feature.settings.presentation.SettingsViewModel
import com.javernaut.whatthecodec.feature.settings.ui.SettingsScreen
import com.javernaut.whatthecodec.home.presentation.model.ActualFrame
import com.javernaut.whatthecodec.home.presentation.model.ActualPreview
import com.javernaut.whatthecodec.home.presentation.model.AudioPage
import com.javernaut.whatthecodec.home.presentation.model.FrameMetrics
import com.javernaut.whatthecodec.home.presentation.model.ScreenState
import com.javernaut.whatthecodec.home.presentation.model.SubtitlesPage
import com.javernaut.whatthecodec.home.presentation.model.VideoPage
import com.javernaut.whatthecodec.home.ui.screen.EmptyHomeScreen
import com.javernaut.whatthecodec.home.ui.screen.MainHomeScreen
import io.github.javernaut.mediafile.AudioStream
import io.github.javernaut.mediafile.BasicStreamInfo
import io.github.javernaut.mediafile.VideoStream
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import io.github.javernaut.mediafile.model.AudioStream
import io.github.javernaut.mediafile.model.BasicStreamInfo
import io.github.javernaut.mediafile.model.BitRate
import io.github.javernaut.mediafile.model.Container
import io.github.javernaut.mediafile.model.FrameRate
import io.github.javernaut.mediafile.model.SampleRate
import io.github.javernaut.mediafile.model.VideoStream
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import org.junit.After
import org.junit.Assume
import org.junit.Before
Expand Down Expand Up @@ -107,23 +113,22 @@ class ScreenshotsTestSuite(

@Test
fun videoTabScreen() {
val basicStreamInfo = mockk<BasicStreamInfo>()
every { basicStreamInfo.index } returns 0
every { basicStreamInfo.title } returns null
every { basicStreamInfo.codecName } returns "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10"
every { basicStreamInfo.language } returns null
every { basicStreamInfo.disposition } returns 0

val videoStream = mockk<VideoStream>()
every { videoStream.basicInfo } returns basicStreamInfo
every { videoStream.frameHeight } returns 1034
every { videoStream.frameWidth } returns 1840
every { videoStream.frameRate } returns 25.0
every { videoStream.bitRate } returns 4_500_000
val videoStream = VideoStream(
BasicStreamInfo(
index = 0,
title = null,
codecName = "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
language = null,
disposition = 0
),
bitRate = BitRate(4_500_000),
frameRate = FrameRate(25.0),
frameSize = Size(1840, 1034)
)

val assets = InstrumentationRegistry.getInstrumentation().context.assets
val preview = ActualPreview(
frameMetrics = FrameMetrics(720, 405),
frameMetrics = Size(720, 405),
frames = (1..4).map {
val inputStream = assets.open("video_frame_$it.png")
val bitmap = BitmapFactory.decodeStream(inputStream)
Expand All @@ -136,8 +141,7 @@ class ScreenshotsTestSuite(
val screenState = ScreenState(
videoPage = VideoPage(
preview,
"QuickTime / MOV",
true,
Container("QuickTime / MOV"),
videoStream,
videoStreamFeatures = completeEnumSet()
),
Expand All @@ -153,20 +157,20 @@ class ScreenshotsTestSuite(

@Test
fun audioTabScreen() {
val basicStreamInfo = mockk<BasicStreamInfo>()
every { basicStreamInfo.index } returns 0
every { basicStreamInfo.title } returns null
every { basicStreamInfo.codecName } returns "MP3 (MPEG audio layer 3)"
every { basicStreamInfo.language } returns null
every { basicStreamInfo.disposition } returns 0

val audioStream = mockk<AudioStream>()
every { audioStream.basicInfo } returns basicStreamInfo
every { audioStream.bitRate } returns 320_000
every { audioStream.sampleFormat } returns "fltp"
every { audioStream.sampleRate } returns 44_100
every { audioStream.channels } returns 2
every { audioStream.channelLayout } returns "stereo"
val audioStream = AudioStream(
BasicStreamInfo(
index = 0,
title = null,
codecName = "MP3 (MPEG audio layer 3)",
language = null,
disposition = 0
),
bitRate = BitRate(320_000),
sampleFormat = "fltp",
sampleRate = SampleRate(44_100),
channels = 2,
channelLayout = "stereo",
)

val screenState = ScreenState(
videoPage = null,
Expand All @@ -183,21 +187,37 @@ class ScreenshotsTestSuite(

@Test
fun settingsScreen() {
val settingsViewModel = mockk<SettingsViewModel>()

val appThemeFlow = MutableStateFlow(AppTheme.Auto)
every { settingsViewModel.appTheme } returns appThemeFlow
val contentSettingsRepository = object : ContentSettingsRepository {
override val videoStreamFeatures: Flow<Set<VideoStreamFeature>>
get() = flowOf(completeEnumSet())
override val audioStreamFeatures: Flow<Set<AudioStreamFeature>>
get() = flowOf(completeEnumSet())
override val subtitleStreamFeatures: Flow<Set<SubtitleStreamFeature>>
get() = flowOf(completeEnumSet())

override suspend fun setVideoStreamFeatures(newVideoStreamFeatures: Set<VideoStreamFeature>) =
Unit

override suspend fun setAudioStreamFeatures(newAudioStreamFeatures: Set<AudioStreamFeature>) =
Unit

override suspend fun setSubtitleStreamFeatures(newSubtitleStreamFeatures: Set<SubtitleStreamFeature>) =
Unit
}

val videoFeatures = MutableStateFlow(completeEnumSet<VideoStreamFeature>())
val audioFeatures = MutableStateFlow(completeEnumSet<AudioStreamFeature>())
val subtitleFeatures = MutableStateFlow(completeEnumSet<SubtitleStreamFeature>())
val themeSettingsRepository = object : ThemeSettingsRepository {
override val themeSettings: Flow<ThemeSettings>
get() = flowOf(ThemeSettings(AppTheme.Auto))

every { settingsViewModel.audioStreamFeatures } returns audioFeatures
every { settingsViewModel.videoStreamFeatures } returns videoFeatures
every { settingsViewModel.subtitleStreamFeatures } returns subtitleFeatures
override suspend fun setSelectedTheme(newAppTheme: AppTheme) = Unit
}

makeScreenshotOf("settings") {
SettingsScreen(settingsViewModel, {}, {})
SettingsScreen(
SettingsViewModel(contentSettingsRepository, themeSettingsRepository),
{},
{}
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,34 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.javernaut.whatthecodec.compose.theme.WhatTheCodecTheme
import com.javernaut.whatthecodec.feature.settings.data.content.completeEnumSet
import com.javernaut.whatthecodec.home.presentation.MediaFileArgument
import com.javernaut.whatthecodec.home.presentation.MediaFileViewModel
import com.javernaut.whatthecodec.home.presentation.MediaFileProvider
import com.javernaut.whatthecodec.home.presentation.MediaType
import com.javernaut.whatthecodec.home.presentation.PreviewLoaderHelper
import com.javernaut.whatthecodec.home.presentation.model.AudioPage
import com.javernaut.whatthecodec.home.presentation.model.ScreenState
import com.javernaut.whatthecodec.home.presentation.model.SubtitlesPage
import com.javernaut.whatthecodec.home.presentation.model.VideoPage
import com.javernaut.whatthecodec.home.ui.screen.EmptyHomeScreen
import com.javernaut.whatthecodec.home.ui.screen.pickAudioFile
import com.javernaut.whatthecodec.home.ui.screen.pickVideoFile
import dagger.hilt.android.AndroidEntryPoint
import io.github.javernaut.mediafile.creator.MediaType
import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.javernaut.mediafile.MediaFile
import io.github.javernaut.mediafile.model.MediaInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject

@AndroidEntryPoint
class ComposePlaygroundActivity : ComponentActivity() {
Expand All @@ -24,27 +43,21 @@ class ComposePlaygroundActivity : ComponentActivity() {

setContent {
WhatTheCodecTheme.Dynamic {
val viewModel = hiltViewModel<MediaFileViewModel>()
val viewModel = hiltViewModel<TestMediaFileViewModel>()

val onAudioIconClick = pickAudioFile(
permissionDenied = viewModel::onPermissionDenied
) {
viewModel.openMediaFile(
MediaFileArgument(
it.toString(),
MediaType.AUDIO
)
MediaFileArgument(it, MediaType.AUDIO)
)
}

val onVideoIconClick = pickVideoFile(
permissionDenied = viewModel::onPermissionDenied
) {
viewModel.openMediaFile(
MediaFileArgument(
it.toString(),
MediaType.VIDEO
)
MediaFileArgument(it, MediaType.VIDEO)
)
}

Expand All @@ -59,3 +72,66 @@ class ComposePlaygroundActivity : ComponentActivity() {
}
}
}

@HiltViewModel
class TestMediaFileViewModel @Inject constructor(
private val previewLoaderHelper: PreviewLoaderHelper,
private val mediaFileProvider: MediaFileProvider
) : ViewModel() {
fun onPermissionDenied() {
// Whatever, it's test
}

private val _screenState = MutableStateFlow<ScreenState?>(null)
val screenState = _screenState.asStateFlow()

fun openMediaFile(mediaFileArgument: MediaFileArgument) {
viewModelScope.launch(Dispatchers.IO) {
val mediaFileContext = mediaFileProvider.obtainMediaFile(
mediaFileArgument.uri
)

val mediaFile = mediaFileContext?.readMediaInfo()

withContext(Dispatchers.Main) {
if (mediaFileContext != null && mediaFile != null) {
fileProcessingFlow(mediaFileContext, mediaFile).collect(_screenState)
}
}
}
}

private fun fileProcessingFlow(
mediaFile: MediaFile,
mediaInfo: MediaInfo
): Flow<ScreenState> = flow {
val audioPage = AudioPage(emptyList(), completeEnumSet())
val subtitlePage = SubtitlesPage(emptyList(), completeEnumSet())
videoPagesFlow(mediaFile, mediaInfo).collect {
emit(
ScreenState(it, audioPage, subtitlePage)
)
}
}

private fun videoPagesFlow(
mediaFile: MediaFile,
mediaInfo: MediaInfo
): Flow<VideoPage?> = flow {
val videoStream = mediaInfo.videoStream
if (videoStream == null) {
emit(null)
} else {
previewLoaderHelper.flowFor(mediaFile, mediaInfo).collect {
emit(
VideoPage(
it,
mediaInfo.container,
videoStream,
completeEnumSet()
)
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.javernaut.whatthecodec.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import javax.inject.Qualifier

@Module
@InstallIn(SingletonComponent::class)
object DispatcherModule {
@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default

@IoDispatcher
@Provides
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO

@MainDispatcher
@Provides
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class DefaultDispatcher

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class IoDispatcher

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class MainDispatcher
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.javernaut.whatthecodec.home.domain

import com.javernaut.whatthecodec.di.IoDispatcher
import com.javernaut.whatthecodec.home.presentation.MediaFileArgument
import com.javernaut.whatthecodec.home.presentation.MediaFileProvider
import io.github.javernaut.mediafile.MediaFile
import io.github.javernaut.mediafile.model.MediaInfo
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import java.io.IOException
import javax.inject.Inject

class FileReadingUseCase @Inject constructor(
private val mediaFileProvider: MediaFileProvider,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) {

suspend fun readFile(arg: MediaFileArgument): Result<ReadingResult> =
withContext(ioDispatcher) {
val mediaFile = mediaFileProvider.obtainMediaFile(arg.uri)
?: return@withContext Result.failure(IOException("Couldn't read the file"))

val mediaInfo = mediaFile.readMediaInfo()
?: return@withContext Result.failure(IOException("Couldn't read the file's meta data"))

Result.success(ReadingResult(mediaFile, mediaInfo))
}

// Reusing the data types from MediaFile library
data class ReadingResult(
val context: MediaFile,
val metaData: MediaInfo
)
}
Loading

0 comments on commit 7c3de1b

Please sign in to comment.