-
Notifications
You must be signed in to change notification settings - Fork 330
Feature: Add minimized video service (YouTube-style PiP) #5423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/bma/minimizeVideoPlayer
Are you sure you want to change the base?
Feature: Add minimized video service (YouTube-style PiP) #5423
Conversation
- Adds a service to display a draggable video overlay outside the app, similar to YouTube or Telegram. - Basic controls included: play/pause, close, maximize. - Handles overlay permissions (SYSTEM_ALERT_WINDOW). - This is a draft implementation for feedback; UX and integration with in-app Compose features may need adjustments.
|
Thank you for your contribution! Here are a few things to check in the PR to ensure it's reviewed as quickly as possible:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome feature, thanks!
I have not tested the code yet, but here are a first set of comments.
| @Composable | ||
| override fun View(modifier: Modifier) { | ||
| val context = LocalContext.current | ||
| val (isMinimized, setMinimized) = remember { mutableStateOf(false) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL we can assigned remembered mutable state like this. Handy!
| ) { | ||
| Icon( | ||
| imageVector = Icons.Default.FullscreenExit, | ||
| contentDescription = "Minimize" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a11y
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh sorry, my comment seems a bit rude, it was not supposed to be sent but was a note for me to not forget. But I forgot to edit it.
I have create a new String CommonStrings.action_minimize with this value. It will be available on develop next time we sync the string (probably in the coming days, but for sure on next Monday).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😂 Nah I undrstanded what you mean you were talking about accessibility and hard coded string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh yes, and the comment should have been i18n of course!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i changed string to unknown and also i add a comment about it needs to be replaced with CommonStrings.action_minimize
app/src/main/AndroidManifest.xml
Outdated
| android:theme="@style/Theme.ElementX" | ||
| tools:targetApi="33"> | ||
|
|
||
| <service |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This declaration should be located in the AndroidManifest of the module where FloatingVideoService is defined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh of course
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and remove the declaration from here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh shoot sorry i forgot about that
yeah now i removed it
app/src/main/AndroidManifest.xml
Outdated
| <!-- To be able to install APK from the application --> | ||
| <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> | ||
|
|
||
| <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This declaration should be located in the AndroidManifest of the module where FloatingVideoService is defined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
| <path | ||
| android:pathData="m256,760 l-56,-56 224,-224 -224,-224 56,-56 224,224 224,-224 56,56 -224,224 224,224 -56,56 -224,-224 -224,224Z" | ||
| android:fillColor="#e3e3e3"/> | ||
| </vector> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should not add new icons but use icons provided by Compound. For this one, it is io.element.android.compound.R.drawable.ic_compound_close.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool
what about minimize and maximize icons it seems Compound dose not have them
i will use something else but i will comment in the code that it need to change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The available compound icons are visible here: https://github.com/element-hq/compound-android/blob/main/compound/screenshots/Compound%20Icons%20-%20Light.png. You may want to use expand and collapse for your use case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
| --> | ||
|
|
||
| <resources> | ||
| <string name="video_playback_error">Video playback error</string> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do not add string manually in the project. For now you can just remove it and use CommonStrings.error_unknown.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
now it's removed.
| }) | ||
|
|
||
| container.addView(overlayContainer) | ||
| return container |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be awesome to use Compose UI to design the View. It will simplify the code a lot (I think), and allow to have previews. Maybe https://gist.github.com/kishan-vadoliya/9fbd1e3c1590de1e4a1a830c5d4edb3f can inspire?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will try that and then I will tell you what happend
thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick update on the floating video overlay refactor. I migrated it from XML/ViewBinding to Compose, but ran into an issue: Compose recompositions don’t work reliably in a Service context, even with proper lifecycle setup. State updates happen, but the UI doesn’t recompose.
Because of that, I switched to a hybrid approach: Compose is used for layout, but window size/aspect ratio updates are applied directly via WindowManager and callbacks instead of relying on recomposition.
Main changes:
-
Dynamic aspect ratio for portrait/landscape
-
Removed controller bar (state-driven updates weren’t working) → replaced with tap-to-play/pause gesture
-
Fixed window resizing when toggling minimized/maximized
-
Improved drag/tap gesture handling
So the overlay is Compose-based now, but with direct view updates for stable behavior in Service.
@bmarty
|
such a cool checks |
-changed minimize buttons text to error_unknown but it need new CommonString -moved service permission and registration from app manifest to mediaViewer manifest
-handling TYPE_PHONE deprivation
-remove extra string of video_playback_error
-remove extra margin
…updates Replace ViewBinding-based floating video UI with Jetpack Compose implementation. Due to Compose recomposition limitations in Service context, implemented hybrid approach using direct view manipulation instead of state-driven UI updates. Key changes: - Migrated floating video overlay from XML layouts to Compose - Implemented dynamic aspect ratio handling for portrait/landscape videos - Added direct WindowManager updates for window resizing instead of relying on recomposition - Removed video controller bar (play/pause, seek bar) as Compose state changes don't trigger recomposition in Service lifecycle - Added tap-to-play/pause gesture as alternative to controller bar - Fixed window sizing issues when toggling between maximized and minimized states - Improved drag gesture handling to prevent conflicts with tap gestures Technical notes: - Compose recomposition does not work reliably in Service context even with proper lifecycle owners set up - State changes (mutableStateOf, mutableFloatStateOf) update values but don't trigger UI recomposition - LaunchedEffect blocks can observe state changes but composable body doesn't recompose - Solution: Use state to track values, but update UI elements (WindowManager, Android Views) directly via callbacks and references instead of relying on Compose recomposition - This hybrid approach allows using Compose for layout while maintaining reliable UI updates in Service environment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the update and the migration to Compose!
I have added more remarks.
I have also tested the feature and it seems to work pretty well. I would maybe add some padding, here the minimized video was displayed at the bottom start of the screen. Also maybe having a way to Play/Pause the video on the minimize video would be awesome. Last point, the View should maybe automatically close at the end of the Playback?
.idea/codeStyles/Project.xml
Outdated
| <emptyLine /> | ||
| </value> | ||
| </option> | ||
| </JavaCodeStyleSettings> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You must revert this change.
I have the same change locally, this is a pain, I have not find a way to get ride of it. The problem is that other developers do not have it so we cannot touch this file on each PR...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh sure i put this file in gitignore from now on.
gradle.properties
Outdated
| org.gradle.java.home=/usr/lib/jvm/java-17-openjdk-amd64 | ||
| org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 | ||
| kotlin.compiler.execution.strategy=in-process | ||
| kotlin.daemon.jvm.options=-Xmx2g |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert all the changes in this file. You can place your local configuration in the file local.properties instead, which is ignore by VCS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My apologies — I’m not sure when these changes were made and committed. It wasn’t intentional.
| import java.io.File | ||
| import androidx.core.net.toUri | ||
|
|
||
| class FloatingVideoService : Service(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We try to avoid long file. Please split the code into multiple file. The UI can be extracted, it will force the code to be a bit more decoupled.
Also it will allow to add preview of the UI, for instance:
@PreviewsDayNight
@Composable
internal fun FloatingVideoOverlayPreview() = ElementPreview {
FloatingVideoOverlay(
// params
)
}The method getVideoUriFromMediaSource could be in a separate file too.
| } | ||
|
|
||
| @OptIn(UnstableApi::class) | ||
| fun getVideoUriFromMediaSource(mediaSource: MediaSource): Uri { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nicer to use an extension, which also allow to simplify the name:
fun MediaSource.getVideoUri(): Uri {
Using a block like with(url) {} could also simplify the code, but I am not a big fan of having several this in a block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
| @OptIn(UnstableApi::class) | ||
| fun getVideoUriFromMediaSource(mediaSource: MediaSource): Uri { | ||
| return try { | ||
| val url = mediaSource.url |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
then you can just remove this line.
| fun getVideoUriFromMediaSource(mediaSource: MediaSource): Uri { | ||
| return try { | ||
| val url = mediaSource.url | ||
| Log.d("VideoPlayer", "MediaSource URL: $url") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use Timber on this project, please do not use Log.
Timber.tag("VideoPlayer").d("MediaSource URL: $url")
Also logging the url should be avoided for privacy reason. The log can be sent by the user to our company, so we avoid logging private data (the url could contains sensitive information)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
and you are right it was there for test.
| .align(Alignment.TopCenter) | ||
| .fillMaxWidth() | ||
| .background( | ||
| brush = androidx.compose.ui.graphics.Brush.verticalGradient( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please use import (there are several place to fix in this file)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| onClose: () -> Unit, | ||
| onToggleFullScreen: () -> Unit | ||
| ) { | ||
| var currentAspectRatio by remember { mutableStateOf(16f / 9f) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use mutableFloatStateOf
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.foundation.layout.size | ||
| import androidx.compose.material.IconButton |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You must use io.element.android.libraries.designsystem.theme.components.IconButton instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
| import androidx.compose.material.IconButton | ||
| import androidx.compose.material.icons.Icons | ||
| import androidx.compose.material.icons.filled.Close | ||
| import androidx.compose.material.icons.filled.Fullscreen |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should use CompoundIcon instead of Icon from material.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
| INSTANCE ?: VideoDataRepository().also { INSTANCE = it } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We used DI on this project, so you should remove this companion and let the DI handle the singleton:
@Inject
@SingleIn(AppScope::class)
class VideoDataRepository {
Then startFloating could have a VideoDataRepository parameter and VideoDataRepository could be injected in FloatingVideoService. You can see how members are injected in the Activites for instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
…or better readability and maintainability. - Replaced custom UI elements with components from io.element.android.libraries.designsystem.theme.components.
- Disabled minimize action until video is ready to prevent crash - Fixed minimized video size issue after refactoring FloatingVideoService
…ject. -Created FloatingVideoServiceBindings (inject(service), videoDataRepository()). -Injected VideoDataRepository in FloatingVideoService, used bindings<...>().inject(this) in onCreate, and replaced singleton calls with DI.
|
for play and pause just tap on the video |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the update. More comments!
|
|
||
| fun MediaSource.getVideoUriFromMediaSource () : Uri{ | ||
| return try { | ||
| val url = this.url |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line can just be removed.
| import timber.log.Timber | ||
| import java.io.File | ||
|
|
||
| fun MediaSource.getVideoUriFromMediaSource () : Uri{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename to getUri? No need to repeat MediaSource as it's the receiver and also no need to precise Video, it can stay generic.
| } | ||
| url.startsWith("/") -> { | ||
| // Local file path, convert to file URI | ||
| Uri.fromFile(File(url)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe change to
| Uri.fromFile(File(url)) | |
| File(url).toUri() |
| isMaximized : Boolean, | ||
| windowManager : WindowManager?, | ||
| windowLayoutParams : WindowManager.LayoutParams, | ||
| floatingView : View?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of providing windowManager and floatingView, isMaximized, I'll provide some lambda parameter and handle all the computation in FloatingVideoService.
At the end, the signature should be something like:
fun FloatingVideoOverlay(
uri: Uri?,
onClose: () -> Unit,
onToggleFullScreen: () -> Unit,
updateAspectRatio: (Float) -> Unit,
movePosition: (Int, Int) -> Unit,
modifier: Modifier = Modifier,
)|
|
||
|
|
||
|
|
||
| var resolvedUri: Uri = Uri.EMPTY |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be remembered
| audioFocus = audioFocus, | ||
| onBackClick = callback::onDone, | ||
| ) | ||
| val data = state.listData |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This need to be moved below
| ) | ||
| val data = state.listData | ||
| .getOrNull(state.currentIndex) as? MediaViewerPageData.MediaViewerData | ||
| Box(modifier = modifier.fillMaxSize()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to add a Box here I think.
| @ContributesTo(AppScope::class) | ||
| interface FloatingVideoServiceBindings { | ||
| fun inject(service: FloatingVideoService) | ||
| fun videoDataRepository(): VideoDataRepository |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fun can be removed if you handle my other remarks.
| } | ||
|
|
||
| private var currentVideoId: String? = null | ||
| private var eventId: String? = null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not used, remove?
| windowLayoutParams.height = height | ||
| windowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or | ||
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | ||
| windowManager?.updateViewLayout(floatingView, windowLayoutParams) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be done twice (here and below), can you check?
|
Thanks for the detailed review. I’ll apply these changes and I really appreciate the explanations. |
What’s Changed
Why This Matters
Many modern messengers (e.g. Telegram, WhatsApp) already support minimized video.
Element X’s structure makes this challenging, but this approach provides a practical way to implement it while staying close to the app’s design principles.
Demo
video_2025-09-27_12-22-19.mp4
Closes #5421
Notes
This was really fun to implement, and I believe it can improve the user experience a lot. 🚀
Also — I’m open to remote Android opportunities. If anyone is interested in collaborating or has a project/position where I can contribute, feel free to reach out: [[email protected]]