Supported div-video

commit_hash:ca1673ef1da065578ca126318498915ccda9b5d3
This commit is contained in:
burstein
2026-05-25 16:55:15 +03:00
parent cd19a33400
commit fc123e035a
55 changed files with 1124 additions and 8 deletions
+34
View File
@@ -603,6 +603,9 @@
"client/android/compose-interop/src/main/java/com/yandex/div/compose/interop/DivViewInterop.kt":"divkit/public/client/android/compose-interop/src/main/java/com/yandex/div/compose/interop/DivViewInterop.kt",
"client/android/compose-lottie/build.gradle.kts":"divkit/public/client/android/compose-lottie/build.gradle.kts",
"client/android/compose-lottie/src/main/kotlin/com/yandex/div/compose/lottie/LottieExtensionHandler.kt":"divkit/public/client/android/compose-lottie/src/main/kotlin/com/yandex/div/compose/lottie/LottieExtensionHandler.kt",
"client/android/compose-video-view-based/build.gradle.kts":"divkit/public/client/android/compose-video-view-based/build.gradle.kts",
"client/android/compose-video-view-based/src/main/kotlin/com/yandex/div/compose/video/viewbased/ViewBasedDivVideoPlayer.kt":"divkit/public/client/android/compose-video-view-based/src/main/kotlin/com/yandex/div/compose/video/viewbased/ViewBasedDivVideoPlayer.kt",
"client/android/compose-video-view-based/src/main/kotlin/com/yandex/div/compose/video/viewbased/ViewBasedDivVideoPlayerFactory.kt":"divkit/public/client/android/compose-video-view-based/src/main/kotlin/com/yandex/div/compose/video/viewbased/ViewBasedDivVideoPlayerFactory.kt",
"client/android/compose/build.gradle.kts":"divkit/public/client/android/compose/build.gradle.kts",
"client/android/compose/proguard-rules.pro":"divkit/public/client/android/compose/proguard-rules.pro",
"client/android/compose/src/androidTest/kotlin/com/yandex/div/compose/DivViewPreview.kt":"divkit/public/client/android/compose/src/androidTest/kotlin/com/yandex/div/compose/DivViewPreview.kt",
@@ -683,6 +686,9 @@
"client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/scroll/ScrollableChildItem.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/scroll/ScrollableChildItem.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/variables/DivVariableMutableState.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/variables/DivVariableMutableState.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/variables/DivVariableAdapter.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/variables/DivVariableAdapter.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayer.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayer.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayerConfig.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayerConfig.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayerFactory.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayerFactory.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivCustomView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivCustomView.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivSelectView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivSelectView.kt",
@@ -735,6 +741,8 @@
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/text/DivTextView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/text/DivTextView.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/text/SpanStyle.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/text/SpanStyle.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/text/TextStyle.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/text/TextStyle.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/video/DivVideoView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/video/DivVideoView.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/video/VideoActionPerformer.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/video/VideoActionPerformer.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewAccessibilityTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewAccessibilityTest.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewRecompositionTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewRecompositionTest.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewTest.kt",
@@ -743,6 +751,7 @@
"client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithExtensionsTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithExtensionsTest.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithFunctionsTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithFunctionsTest.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithTriggersTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithTriggersTest.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithVideoTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithVideoTest.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithVisibilityActionsRecompositionTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithVisibilityActionsRecompositionTest.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithVisibilityActionsTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithVisibilityActionsTest.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/IntegrationTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/IntegrationTest.kt",
@@ -5585,6 +5594,9 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_div_input_selection.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_div_input_selection.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background_and_border.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background_and_border.png",
@@ -7505,6 +7517,9 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_div_input_selection.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_div_input_selection.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background_and_border.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background_and_border.png",
@@ -9425,6 +9440,9 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_div_input_selection.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_div_input_selection.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background_and_border.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background_and_border.png",
@@ -11345,6 +11363,9 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_div_input_selection.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_div_input_selection.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background_and_border.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background_and_border.png",
@@ -13265,6 +13286,9 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_div_input_selection.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_div_input_selection.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background_and_border.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background_and_border.png",
@@ -15185,6 +15209,9 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_div_input_selection.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_div_input_selection.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background_and_border.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background_and_border.png",
@@ -17105,6 +17132,9 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_div_input_selection.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_div_input_selection.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background_and_border.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2FocusScreenshotTest/focused/snapshot_with_blurred_and_focused_background_and_border.png",
@@ -19025,6 +19055,9 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_left_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_45_around_top_right_corner.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-transform/transform_turn_90_around_center.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png",
"client/android/divkit-demo-app/ui-tests.gradle":"divkit/public/client/android/divkit-demo-app/ui-tests.gradle",
"client/android/divkit-perftests/build.gradle":"divkit/public/client/android/divkit-perftests/build.gradle",
"client/android/divkit-perftests/proguard-rules.pro":"divkit/public/client/android/divkit-perftests/proguard-rules.pro",
@@ -19223,6 +19256,7 @@
"client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivTriggerUtils.kt":"divkit/public/client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivTriggerUtils.kt",
"client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivTypedValueUtils.kt":"divkit/public/client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivTypedValueUtils.kt",
"client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivVariableUtils.kt":"divkit/public/client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivVariableUtils.kt",
"client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivVideoUtils.kt":"divkit/public/client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivVideoUtils.kt",
"client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/ExpressionUtils.kt":"divkit/public/client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/ExpressionUtils.kt",
"client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/Utils.kt":"divkit/public/client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/Utils.kt",
"client/android/ui-test-common/build.gradle.kts":"divkit/public/client/android/ui-test-common/build.gradle.kts",
+1
View File
@@ -113,6 +113,7 @@ apiValidation {
"api-generator-test",
"compose",
"compose-lottie",
"compose-video-view-based",
"divkit-benchmark-app",
"divkit-demo-app",
"divkit-perftests",
@@ -0,0 +1,25 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose)
}
apply(from = "../div-library.gradle")
apply(from = "../publish-android.gradle")
android {
namespace = "com.yandex.div.compose.video.viewbased"
defaultConfig {
minSdk = 23
}
}
dependencies {
implementation(project(":compose"))
implementation(project(":div"))
implementation(project(":div-data"))
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.coreKtx)
}
@@ -0,0 +1,163 @@
package com.yandex.div.compose.video.viewbased
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import com.yandex.div.compose.video.DivVideoPlayer
import com.yandex.div.compose.video.DivVideoPlayerConfig
import com.yandex.div.compose.video.DivVideoSource
import com.yandex.div.core.player.DivPlayer
import com.yandex.div.core.player.DivPlayerFactory
import com.yandex.div.core.player.DivPlayerPlaybackConfig
import com.yandex.div.core.player.DivVideoResolution as PlayerDivVideoResolution
import com.yandex.div.core.player.DivVideoSource as PlayerDivVideoSource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* Bridges any `View`-based [DivPlayer] implementation into the Compose-native
* [DivVideoPlayer] surface.
*
* Translates the delegate's callback-based [DivPlayer.Observer] into [StateFlow]
* properties, and forwards [DivVideoPlayerConfig] updates handed to [Content] to
* the delegate via `setSource` / `setMuted` / `setPlaybackSpeed` / `seek`.
*
* The underlying delegate is created lazily on the first composition of [Content]:
* some `DivPlayer` implementations need a non-empty source list at construction time
* to wire their internal listeners, and only at composition time do we have the real
* config.
*/
internal class ViewBasedDivVideoPlayer(
private val delegateFactory: DivPlayerFactory
) : DivVideoPlayer {
private val _isReady = MutableStateFlow(false)
override val isReady: StateFlow<Boolean> = _isReady
private val _isPlaying = MutableStateFlow(false)
override val isPlaying: StateFlow<Boolean> = _isPlaying
private val _isBuffering = MutableStateFlow(false)
override val isBuffering: StateFlow<Boolean> = _isBuffering
private val _isEnded = MutableStateFlow(false)
override val isEnded: StateFlow<Boolean> = _isEnded
private val _error = MutableStateFlow<Throwable?>(null)
override val error: StateFlow<Throwable?> = _error
private val _currentTimeMs = MutableStateFlow(0L)
override val currentTimeMs: StateFlow<Long> = _currentTimeMs
private var delegate: DivPlayer? = null
private val stateObserver = object : DivPlayer.Observer {
override fun onReady() {
_isReady.value = true
_isBuffering.value = false
}
override fun onPlay() {
_isPlaying.value = true
_isEnded.value = false
}
override fun onPause() {
_isPlaying.value = false
}
override fun onBuffering() {
_isBuffering.value = true
}
override fun onEnd() {
_isPlaying.value = false
_isEnded.value = true
}
override fun onFatal(error: Throwable) {
_error.value = error
}
override fun onCurrentTimeChange(timeMs: Long) {
_currentTimeMs.value = timeMs
}
}
override fun play() {
delegate?.play()
}
override fun pause() {
delegate?.pause()
}
override fun seek(timeMs: Long) {
delegate?.seek(timeMs)
}
override fun release() {
delegate?.let {
it.removeObserver(stateObserver)
it.release()
delegate = null
}
}
@Composable
override fun Content(config: DivVideoPlayerConfig, modifier: Modifier) {
val player = remember {
delegateFactory.makePlayer(
config.sources.toPlayerSources(),
config.toPlayerPlaybackConfig(),
).also {
it.addObserver(stateObserver)
delegate = it
}
}
LaunchedEffect(config.sources, config.autoplay, config.repeatable, config.payload) {
player.setSource(config.sources.toPlayerSources(), config.toPlayerPlaybackConfig())
}
LaunchedEffect(config.muted) { player.setMuted(config.muted) }
LaunchedEffect(config.playbackSpeed) { player.setPlaybackSpeed(config.playbackSpeed) }
AndroidView(
modifier = modifier,
factory = { ctx ->
delegateFactory.makePlayerView(ctx).apply {
attach(player)
setScale(config.scale)
}
},
update = { view ->
view.setScale(config.scale)
if (view.getAttachedPlayer() !== player) {
view.detach()
view.attach(player)
}
},
onRelease = { view -> view.detach() },
)
}
}
private fun List<DivVideoSource>.toPlayerSources(): List<PlayerDivVideoSource> = map { source ->
PlayerDivVideoSource(
url = source.url,
mimeType = source.mimeType,
resolution = source.resolution?.let { PlayerDivVideoResolution(it.width, it.height) },
bitrate = source.bitrate,
)
}
private fun DivVideoPlayerConfig.toPlayerPlaybackConfig(): DivPlayerPlaybackConfig =
DivPlayerPlaybackConfig(
autoplay = autoplay,
isMuted = muted,
repeatable = repeatable,
payload = payload,
playbackSpeed = playbackSpeed,
)
@@ -0,0 +1,30 @@
package com.yandex.div.compose.video.viewbased
import com.yandex.div.compose.video.DivVideoPlayer
import com.yandex.div.compose.video.DivVideoPlayerFactory
import com.yandex.div.core.annotations.ExperimentalApi
import com.yandex.div.core.player.DivPlayerFactory
/**
* Compose-native adapter that exposes any `View`-based [DivPlayerFactory] (e.g. the
* media3 ExoPlayer factory or the Yandex `ListYandexPlayer` factory) through the
* Compose-native [DivVideoPlayerFactory] surface.
*
* The produced [DivVideoPlayer] renders itself via [androidx.compose.ui.viewinterop.AndroidView]
* interop wrapping the underlying [com.yandex.div.core.player.DivPlayerView]. A future
* pure-Compose implementation would use a Compose-native player surface instead.
*
* Usage:
* ```
* val configuration = DivComposeConfiguration(
* playerFactory = ViewBasedDivVideoPlayerFactory(ExoDivPlayerFactory(context)),
* ...
* )
* ```
*/
@ExperimentalApi
class ViewBasedDivVideoPlayerFactory(
private val delegateFactory: DivPlayerFactory,
) : DivVideoPlayerFactory {
override fun makePlayer(): DivVideoPlayer = ViewBasedDivVideoPlayer(delegateFactory)
}
@@ -1,6 +1,8 @@
package com.yandex.div.compose
import android.graphics.Typeface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import com.yandex.div.compose.actions.DivExternalActionHandler
import com.yandex.div.compose.custom.DivCustomViewFactory
@@ -8,10 +10,15 @@ import com.yandex.div.compose.dagger.Names
import com.yandex.div.compose.extensions.DivExtensionHandler
import com.yandex.div.compose.font.DivFontSource
import com.yandex.div.compose.font.DivFontSourceProvider
import com.yandex.div.compose.video.DivVideoPlayer
import com.yandex.div.compose.video.DivVideoPlayerConfig
import com.yandex.div.compose.video.DivVideoPlayerFactory
import com.yandex.div.core.annotations.ExperimentalApi
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.yatagan.Module
import com.yandex.yatagan.Provides
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Named
/**
@@ -42,6 +49,9 @@ class DivComposeConfiguration(
@get:Provides
val fontSourceProvider: DivFontSourceProvider = defaultFontSourceProvider,
@get:Provides
val playerFactory: DivVideoPlayerFactory = defaultDivVideoPlayerFactory,
@get:Provides
val reporter: DivReporter = DivReporter(),
@@ -57,3 +67,20 @@ private val defaultFontSourceProvider = object : DivFontSourceProvider {
return DivFontSource.Typeface(Typeface.DEFAULT)
}
}
private val defaultDivVideoPlayerFactory = DivVideoPlayerFactory {
object : DivVideoPlayer {
override val isReady: StateFlow<Boolean> = MutableStateFlow(false)
override val isPlaying: StateFlow<Boolean> = MutableStateFlow(false)
override val isBuffering: StateFlow<Boolean> = MutableStateFlow(false)
override val isEnded: StateFlow<Boolean> = MutableStateFlow(false)
override val error: StateFlow<Throwable?> = MutableStateFlow(null)
override val currentTimeMs: StateFlow<Long> = MutableStateFlow(0L)
override fun seek(timeMs: Long) = Unit
override fun play() = Unit
override fun pause() = Unit
override fun release() = Unit
@Composable
override fun Content(config: DivVideoPlayerConfig, modifier: Modifier) = Unit
}
}
@@ -13,5 +13,6 @@ enum class DivActionSource {
LONG_TAP,
TAP,
TRIGGER,
VIDEO,
VISIBILITY
}
@@ -12,6 +12,7 @@ import com.yandex.div.compose.font.DivFontSourceProvider
import com.yandex.div.compose.images.ImageRequestListener
import com.yandex.div.compose.internal.DivDebugConfiguration
import com.yandex.div.compose.internal.DivDebugFeatures
import com.yandex.div.compose.video.DivVideoPlayerFactory
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.yatagan.BindsInstance
import com.yandex.yatagan.Component
@@ -35,6 +36,7 @@ internal interface DivContextComponent {
val fontFamilyCache: DivFontFamilyCache
val imageLoader: ImageLoader
val imageRequestListener: ImageRequestListener
val playerFactory: DivVideoPlayerFactory
val viewContextStorage: DivViewContextStorage
@get:Named(Names.HOST_VARIABLES)
@@ -20,6 +20,11 @@ internal fun mutableStateFromVariable(variableName: String, defaultValue: Boolea
return mutableStateFromBooleanVariable(variableName) ?: remember { mutableStateOf(defaultValue) }
}
@Composable
internal fun mutableStateFromVariable(variableName: String, defaultValue: Long): MutableState<Long> {
return mutableStateFromIntegerVariable(variableName) ?: remember { mutableStateOf(defaultValue) }
}
@Composable
internal fun mutableStateFromStringVariable(variableName: String): MutableState<String>? {
return rememberVariableMutableState(
@@ -44,6 +49,18 @@ internal fun mutableStateFromBooleanVariable(variableName: String): MutableState
)
}
@Composable
internal fun mutableStateFromIntegerVariable(variableName: String): MutableState<Long>? {
return rememberVariableMutableState(
variableName = variableName,
readValue = { it.getValue() as Long },
validate = { variable ->
if (variable is Variable.IntegerVariable) null
else "variable [$variableName] is not an integer variable"
},
)
}
@Composable
private fun <T : Any> rememberVariableMutableState(
variableName: String,
@@ -0,0 +1,72 @@
package com.yandex.div.compose.video
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.yandex.div.core.annotations.ExperimentalApi
import kotlinx.coroutines.flow.StateFlow
/**
* Compose-native player abstraction for `div-video`.
*
* Inputs (sources, playback options, scale, ...) are pushed into the player by passing
* a [DivVideoPlayerConfig] to [Content] on every composition. Outputs — playback state
* and errors — are exposed as [StateFlow] so they can be consumed both from coroutines
* and from `@Composable` functions via `collectAsState`.
*
* Implementations are free to render either via [androidx.compose.ui.viewinterop.AndroidView]
* interop (for existing `View`-based players such as media3) or natively in Compose.
*/
@ExperimentalApi
interface DivVideoPlayer {
/**
* `true` once the player is ready to render frames.
*/
val isReady: StateFlow<Boolean>
/**
* `true` while the player is actively playing.
*/
val isPlaying: StateFlow<Boolean>
/**
* `true` while the player is buffering.
*/
val isBuffering: StateFlow<Boolean>
/**
* `true` when playback has reached the end.
*/
val isEnded: StateFlow<Boolean>
/**
* Last fatal error, or `null` if no error has occurred.
*/
val error: StateFlow<Throwable?>
/**
* Current playback position in milliseconds; updated by the player as playback
* progresses. Use [seek] to change the position.
*/
val currentTimeMs: StateFlow<Long>
fun play()
fun pause()
/**
* Jumps playback to the given position. Subsequent updates to [currentTimeMs] reflect
* the new position.
*/
fun seek(timeMs: Long)
fun release()
/**
* Renders the visual representation of the player and applies [config].
*
* Changes to [config] between subsequent calls drive the underlying playback engine.
*/
@Composable
fun Content(config: DivVideoPlayerConfig, modifier: Modifier)
}
@@ -0,0 +1,43 @@
package com.yandex.div.compose.video
import android.net.Uri
import com.yandex.div.core.annotations.ExperimentalApi
import com.yandex.div2.DivVideoScale
import org.json.JSONObject
/**
* Input parameters supplied to a [DivVideoPlayer] via [DivVideoPlayer.Content].
*
* Fed into the player on every composition; the player observes changes between
* subsequent values and reacts (e.g. starts a new source, mutes, changes speed).
*/
@ExperimentalApi
data class DivVideoPlayerConfig(
val sources: List<DivVideoSource> = emptyList(),
val autoplay: Boolean = false,
val repeatable: Boolean = false,
val payload: JSONObject? = null,
val muted: Boolean = false,
val playbackSpeed: Float = 1f,
val scale: DivVideoScale = DivVideoScale.FIT,
)
/**
* Single source variant passed to a [DivVideoPlayer].
*/
@ExperimentalApi
data class DivVideoSource(
val url: Uri,
val mimeType: String = "",
val resolution: DivVideoResolution? = null,
val bitrate: Long? = null,
)
/**
* Optional resolution metadata for a [DivVideoSource].
*/
@ExperimentalApi
data class DivVideoResolution(
val width: Int,
val height: Int,
)
@@ -0,0 +1,17 @@
package com.yandex.div.compose.video
import com.yandex.div.core.annotations.ExperimentalApi
/**
* Factory that produces a [DivVideoPlayer].
*
* The returned player applies configuration when its [DivVideoPlayer.Content] composable
* receives a [DivVideoPlayerConfig]; the factory itself doesn't take any input.
*
* @see com.yandex.div.compose.DivComposeConfiguration
*/
@ExperimentalApi
fun interface DivVideoPlayerFactory {
fun makePlayer(): DivVideoPlayer
}
@@ -8,17 +8,18 @@ import com.yandex.div.compose.dagger.LocalComponent
import com.yandex.div.compose.dagger.WithLocalComponent
import com.yandex.div.compose.expressions.observedValue
import com.yandex.div.compose.extensions.DivExtensionEnvironment
import com.yandex.div.compose.utils.reportError
import com.yandex.div.compose.views.container.DivContainerView
import com.yandex.div.compose.views.gallery.DivGalleryView
import com.yandex.div.compose.views.grid.DivGridView
import com.yandex.div.compose.views.image.DivImageView
import com.yandex.div.compose.views.input.DivInputView
import com.yandex.div.compose.views.pager.DivPagerView
import com.yandex.div.compose.views.modifiers.apply
import com.yandex.div.compose.views.modifiers.applyPaddings
import com.yandex.div.compose.views.pager.DivPagerView
import com.yandex.div.compose.utils.reportError
import com.yandex.div.compose.views.input.DivInputView
import com.yandex.div.compose.views.state.DivStateView
import com.yandex.div.compose.views.text.DivTextView
import com.yandex.div.compose.views.video.DivVideoView
import com.yandex.div2.Div
import com.yandex.div2.DivExtension
import com.yandex.div2.DivVisibility
@@ -101,11 +102,11 @@ private fun BaseView(
is Div.State -> DivStateView(modifier, data.value)
is Div.Switch -> DivSwitchView(modifier.applyPaddings(data), data.value)
is Div.Text -> DivTextView(modifier.applyPaddings(data), data.value)
is Div.Video -> DivVideoView(modifier.applyPaddings(data), data.value)
is Div.GifImage -> NotSupported("gif")
is Div.Indicator -> NotSupported("indicator")
is Div.Slider -> NotSupported("slider")
is Div.Tabs -> NotSupported("tabs")
is Div.Video -> NotSupported("video")
}
}
@@ -0,0 +1,140 @@
package com.yandex.div.compose.views.video
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.FixedScale
import androidx.compose.ui.platform.LocalDensity
import coil3.ImageLoader
import coil3.compose.rememberAsyncImagePainter
import com.yandex.div.compose.context.divContext
import com.yandex.div.compose.dagger.LocalComponent
import com.yandex.div.compose.expressions.observedValue
import com.yandex.div.compose.images.ImageRequestParams
import com.yandex.div.compose.images.decodePreview
import com.yandex.div.compose.images.rememberImageRequest
import com.yandex.div.compose.utils.reportError
import com.yandex.div.compose.utils.variables.mutableStateFromIntegerVariable
import com.yandex.div.compose.video.DivVideoPlayer
import com.yandex.div.compose.video.DivVideoPlayerConfig
import com.yandex.div.compose.video.DivVideoResolution
import com.yandex.div.compose.video.DivVideoSource
import com.yandex.div2.DivVideo
import com.yandex.div2.DivVideoScale
import com.yandex.div2.DivVideoSource as Div2VideoSource
@Composable
internal fun DivVideoView(modifier: Modifier, data: DivVideo) {
val context = divContext
val config = data.observedConfig()
if (config.sources.isEmpty() && data.playerSettingsPayload == null) {
reportError(
"Neither 'video_source' nor 'player_settings_payload' are specified for video with id '${data.id}'"
)
}
val player = remember { context.component.playerFactory.makePlayer() }
DisposableEffect(player) {
onDispose { player.release() }
}
ObserveVideoActions(
player = player,
data = data,
actionHandler = context.component.actionHandler,
actionHandlingContext = LocalComponent.current.actionHandlingContext,
)
data.elapsedTimeVariable?.let { variableName ->
ObserveElapsedTimeVariable(player = player, variableName = variableName)
}
Box(modifier = modifier) {
player.Content(config = config, modifier = Modifier.fillMaxSize())
if (!player.isReady.collectAsState().value) {
data.preview?.let {
VideoPreviewImage(
imageLoader = divContext.component.imageLoader,
scale = config.scale,
preview = it.observedValue(transform = ::decodePreview)
)
}
}
}
}
@Composable
private fun ObserveElapsedTimeVariable(player: DivVideoPlayer, variableName: String) {
val variableState = mutableStateFromIntegerVariable(variableName) ?: return
LaunchedEffect(player) {
player.currentTimeMs.collect { timeMs ->
variableState.value = timeMs
}
}
LaunchedEffect(variableState.value) {
if (variableState.value != player.currentTimeMs.value) {
player.seek(variableState.value)
}
}
}
@Composable
private fun VideoPreviewImage(imageLoader: ImageLoader, scale: DivVideoScale, preview: ByteArray) {
val density = LocalDensity.current.density
val contentScale = scale.toContentScale(density)
val imageRequest = rememberImageRequest(
ImageRequestParams(data = preview, transformations = emptyList())
)
Image(
modifier = Modifier.fillMaxSize(),
painter = rememberAsyncImagePainter(model = imageRequest, imageLoader = imageLoader),
contentDescription = null,
contentScale = contentScale,
)
}
@Composable
private fun DivVideo.observedConfig(): DivVideoPlayerConfig {
return DivVideoPlayerConfig(
sources = videoSources.orEmpty().map { it.observedPlayerSource() },
autoplay = autostart.observedValue(),
repeatable = repeatable.observedValue(),
payload = playerSettingsPayload?.observedValue(),
muted = muted.observedValue(),
playbackSpeed = playbackSpeed.observedValue().toFloat(),
scale = scale.observedValue(),
)
}
@Composable
private fun Div2VideoSource.observedPlayerSource(): DivVideoSource {
return DivVideoSource(
url = url.observedValue(),
mimeType = mimeType.observedValue(),
resolution = resolution?.let {
DivVideoResolution(
width = it.width.observedValue().toInt(),
height = it.height.observedValue().toInt(),
)
},
bitrate = bitrate?.observedValue(),
)
}
private fun DivVideoScale.toContentScale(density: Float): ContentScale = when (this) {
DivVideoScale.FILL -> ContentScale.Crop
DivVideoScale.FIT -> ContentScale.Fit
DivVideoScale.NO_SCALE -> FixedScale(density)
}
@@ -0,0 +1,83 @@
package com.yandex.div.compose.views.video
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.yandex.div.compose.DivReporter
import com.yandex.div.compose.actions.DivActionHandler
import com.yandex.div.compose.actions.DivActionHandlingContext
import com.yandex.div.compose.actions.DivActionSource
import com.yandex.div.compose.dagger.LocalComponent
import com.yandex.div.compose.video.DivVideoPlayer
import com.yandex.div2.DivAction
import com.yandex.div2.DivVideo
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
@Composable
internal fun ObserveVideoActions(
player: DivVideoPlayer,
data: DivVideo,
actionHandler: DivActionHandler,
actionHandlingContext: DivActionHandlingContext,
) {
val reporter = LocalComponent.current.reporter
LaunchedEffect(player, data) {
val performer = VideoActionPerformer(player, data, actionHandler, actionHandlingContext, reporter)
performer.perform()
}
}
private class VideoActionPerformer(
private val player: DivVideoPlayer,
private val data: DivVideo,
private val actionHandler: DivActionHandler,
private val actionHandlingContext: DivActionHandlingContext,
private val reporter: DivReporter,
) {
suspend fun perform() = coroutineScope {
launch { observePlayingChanges() }
launch { observeBufferingChanges() }
launch { observeEndedChanges() }
launch { observeErrors() }
}
private suspend fun observePlayingChanges() {
player.isPlaying.drop(1).collect { playing ->
if (playing) {
performActions(data.resumeActions)
} else if (!player.isEnded.value) {
performActions(data.pauseActions)
}
}
}
private suspend fun observeBufferingChanges() {
player.isBuffering.drop(1).collect { buffering ->
if (buffering) performActions(data.bufferingActions)
}
}
private suspend fun observeEndedChanges() {
player.isEnded.drop(1).collect { ended ->
if (ended) performActions(data.endActions)
}
}
private suspend fun observeErrors() {
player.error.collect { err ->
if (err != null) {
reporter.reportError(
"Playback in div with id '${data.id}' encountered an error: ${err.message}"
)
performActions(data.fatalActions)
}
}
}
private fun performActions(actions: List<DivAction>?) {
if (actions.isNullOrEmpty()) return
actionHandler.handle(actionHandlingContext, actions, source = DivActionSource.VIDEO)
}
}
@@ -0,0 +1,381 @@
package com.yandex.div.compose
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.yandex.div.compose.actions.DivActionData
import com.yandex.div.compose.actions.DivActionHandlingContext
import com.yandex.div.compose.actions.DivExternalActionHandler
import com.yandex.div.compose.video.DivVideoPlayer
import com.yandex.div.compose.video.DivVideoPlayerConfig
import com.yandex.div.compose.video.DivVideoPlayerFactory
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.div.data.Variable
import com.yandex.div.test.data.action
import com.yandex.div.test.data.constant
import com.yandex.div.test.data.data
import com.yandex.div.test.data.video
import com.yandex.div.test.data.videoSource
import com.yandex.div2.Div
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.junit.Rule
import org.junit.runner.RunWith
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
@RunWith(AndroidJUnit4::class)
class DivViewWithVideoTest {
@get:Rule
val rule = createComposeRule()
private val reporter = TestReporter()
private val variableController = DivVariableController()
private val playerFactory = FakeDivVideoPlayerFactory()
private val externalActionHandler = RecordingExternalActionHandler()
private val configuration = DivComposeConfiguration(
actionHandler = externalActionHandler,
playerFactory = playerFactory,
reporter = reporter,
variableController = variableController,
)
@Test
fun `error is reported if no sources and no payload`() {
reporter.failOnError = false
setContent(
video(id = "video")
)
assertEquals(
"Neither 'video_source' nor 'player_settings_payload' are specified for video with id 'video'",
reporter.lastError
)
}
@Test
fun `no error reported when only player_settings_payload is set`() {
setContent(
video(playerSettingsPayload = constant(org.json.JSONObject()))
)
assertNull(reporter.lastError)
}
@Test
fun `no error reported when only video_sources is set`() {
setContent(
video(videoSources = listOf(videoSource()))
)
assertNull(reporter.lastError)
}
@Test
fun `release is called when video leaves composition`() {
val visible = mutableStateOf(true)
val divData = data(video(videoSources = listOf(videoSource())))
rule.setContent {
val divContext = DivContext(
baseContext = LocalContext.current,
configuration = configuration,
)
CompositionLocalProvider(LocalContext provides divContext) {
Box {
if (visible.value) {
DivView(data = divData)
}
}
}
}
rule.waitForIdle()
val player = playerFactory.lastCreatedPlayer!!
assertFalse(player.released)
visible.value = false
rule.waitForIdle()
assertTrue(player.released)
}
@Test
fun `config sources reflect sources from data`() {
setContent(
video(videoSources = listOf(videoSource(mimeType = constant("video/webm"))))
)
rule.waitForIdle()
val sources = playerFactory.lastCreatedPlayer!!.lastConfig!!.sources
assertEquals(1, sources.size)
assertEquals("video/webm", sources[0].mimeType)
}
@Test
fun `config muted reflects muted value from data`() {
setContent(
video(
muted = constant(true),
videoSources = listOf(videoSource()),
)
)
rule.waitForIdle()
assertEquals(true, playerFactory.lastCreatedPlayer!!.lastConfig!!.muted)
}
@Test
fun `config playbackSpeed reflects speed from data`() {
setContent(
video(
playbackSpeed = constant(1.5),
videoSources = listOf(videoSource()),
)
)
rule.waitForIdle()
assertEquals(1.5f, playerFactory.lastCreatedPlayer!!.lastConfig!!.playbackSpeed)
}
@Test
fun `resume actions are invoked when player starts playing`() {
setContent(
video(
resumeActions = listOf(action(id = "resume", url = "test://resume")),
videoSources = listOf(videoSource()),
)
)
rule.waitForIdle()
playerFactory.lastCreatedPlayer!!.isPlayingState.value = true
rule.waitForIdle()
assertEquals("resume", externalActionHandler.lastAction?.id)
}
@Test
fun `pause actions are invoked when player pauses without ending`() {
setContent(
video(
pauseActions = listOf(action(id = "pause", url = "test://pause")),
videoSources = listOf(videoSource()),
)
)
rule.waitForIdle()
val player = playerFactory.lastCreatedPlayer!!
player.isPlayingState.value = true
rule.waitForIdle()
player.isPlayingState.value = false
rule.waitForIdle()
assertEquals("pause", externalActionHandler.lastAction?.id)
}
@Test
fun `pause actions are not invoked when player has ended`() {
setContent(
video(
pauseActions = listOf(action(id = "pause", url = "test://pause")),
videoSources = listOf(videoSource()),
)
)
rule.waitForIdle()
val player = playerFactory.lastCreatedPlayer!!
player.isPlayingState.value = true
rule.waitForIdle()
player.isEndedState.value = true
player.isPlayingState.value = false
rule.waitForIdle()
assertNull(externalActionHandler.lastAction)
}
@Test
fun `buffering actions are invoked when player buffers`() {
setContent(
video(
bufferingActions = listOf(action(id = "buffer", url = "test://buffer")),
videoSources = listOf(videoSource()),
)
)
rule.waitForIdle()
playerFactory.lastCreatedPlayer!!.isBufferingState.value = true
rule.waitForIdle()
assertEquals("buffer", externalActionHandler.lastAction?.id)
}
@Test
fun `end actions are invoked when player ends`() {
setContent(
video(
endActions = listOf(action(id = "end", url = "test://end")),
videoSources = listOf(videoSource()),
)
)
rule.waitForIdle()
playerFactory.lastCreatedPlayer!!.isEndedState.value = true
rule.waitForIdle()
assertEquals("end", externalActionHandler.lastAction?.id)
}
@Test
fun `fatal actions are invoked when player errors out`() {
reporter.failOnError = false
setContent(
video(
id = "video",
fatalActions = listOf(action(id = "fatal", url = "test://fatal")),
videoSources = listOf(videoSource()),
)
)
rule.waitForIdle()
playerFactory.lastCreatedPlayer!!.errorState.value = IllegalStateException("boom")
rule.waitForIdle()
assertEquals("fatal", externalActionHandler.lastAction?.id)
}
@Test
fun `error is reported when player encounters an error`() {
reporter.failOnError = false
setContent(
video(
id = "video",
videoSources = listOf(videoSource()),
)
)
rule.waitForIdle()
playerFactory.lastCreatedPlayer!!.errorState.value = IllegalStateException("boom")
rule.waitForIdle()
assertEquals(
"Playback in div with id 'video' encountered an error: boom",
reporter.lastError
)
}
@Test
fun `elapsed_time_variable is updated from player time`() {
val elapsed = Variable.IntegerVariable("elapsed", 0)
variableController.declare(elapsed)
setContent(
video(
elapsedTimeVariable = "elapsed",
videoSources = listOf(videoSource()),
)
)
rule.waitForIdle()
playerFactory.lastCreatedPlayer!!.currentTimeMsState.value = 5_000L
rule.waitForIdle()
assertEquals(5_000L, elapsed.getValue())
}
@Test
fun `player seeks when elapsed_time_variable changes`() {
val elapsed = Variable.IntegerVariable("elapsed", 0)
variableController.declare(elapsed)
setContent(
video(
elapsedTimeVariable = "elapsed",
videoSources = listOf(videoSource()),
)
)
rule.waitForIdle()
elapsed.set(7_500L)
rule.waitForIdle()
assertEquals(7_500L, playerFactory.lastCreatedPlayer!!.currentTimeMs.value)
}
private fun setContent(content: Div) {
rule.setContent(
configuration = configuration,
data = data(content)
)
}
}
private class RecordingExternalActionHandler : DivExternalActionHandler {
var lastAction: DivActionData? = null
private set
override fun handle(context: DivActionHandlingContext, action: DivActionData) {
lastAction = action
}
}
private class FakeDivVideoPlayerFactory : DivVideoPlayerFactory {
var lastCreatedPlayer: FakeDivVideoPlayer? = null
private set
override fun makePlayer(): DivVideoPlayer {
return FakeDivVideoPlayer().also { lastCreatedPlayer = it }
}
}
private class FakeDivVideoPlayer : DivVideoPlayer {
val isReadyState = MutableStateFlow(false)
val isPlayingState = MutableStateFlow(false)
val isBufferingState = MutableStateFlow(false)
val isEndedState = MutableStateFlow(false)
val currentTimeMsState = MutableStateFlow(0L)
val errorState = MutableStateFlow<Throwable?>(null)
override val isReady: StateFlow<Boolean> = isReadyState
override val isPlaying: StateFlow<Boolean> = isPlayingState
override val isBuffering: StateFlow<Boolean> = isBufferingState
override val isEnded: StateFlow<Boolean> = isEndedState
override val currentTimeMs: StateFlow<Long> = currentTimeMsState
override val error: StateFlow<Throwable?> = errorState
var lastConfig: DivVideoPlayerConfig? = null
private set
var released: Boolean = false
private set
override fun play() = Unit
override fun pause() = Unit
override fun seek(timeMs: Long) {
currentTimeMsState.value = timeMs
}
override fun release() {
released = true
}
@Composable
override fun Content(config: DivVideoPlayerConfig, modifier: Modifier) {
lastConfig = config
}
}
@@ -89,6 +89,7 @@ dependencies {
implementation project(path: ':coil')
implementation project(path: ':compose')
implementation project(path: ':compose-lottie')
implementation project(path: ':compose-video-view-based')
implementation project(path: ':div')
implementation project(path: ':div-gesture')
implementation project(path: ':div-lottie')
@@ -8,10 +8,10 @@ import androidx.compose.ui.platform.ComposeView
import androidx.core.net.toUri
import com.yandex.div.DivDataTag
import com.yandex.div.compose.DivComposeConfiguration
import com.yandex.div.compose.DivView as ComposeDivView
import com.yandex.div.compose.DivContext as ComposeDivContext
import com.yandex.div.compose.video.viewbased.ViewBasedDivVideoPlayerFactory
import com.yandex.div.core.Div2Context
import com.yandex.div.core.annotations.InternalApi
import com.yandex.div.video.m3.ExoDivPlayerFactory
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.div.core.view2.Div2View
import com.yandex.div.data.DivParsingEnvironment
@@ -23,6 +23,8 @@ import com.yandex.divkit.demo.font.ComposeFontSourceProvider
import com.yandex.divkit.demo.utils.applyPatchByConfig
import org.json.JSONException
import org.json.JSONObject
import com.yandex.div.compose.DivContext as ComposeDivContext
import com.yandex.div.compose.DivView as ComposeDivView
interface DemoRendererFacade {
val view: View
@@ -107,6 +109,7 @@ class ComposeRendererFacade(
baseContext = context,
configuration = DivComposeConfiguration(
fontSourceProvider = ComposeFontSourceProvider(),
playerFactory = ViewBasedDivVideoPlayerFactory(ExoDivPlayerFactory(context)),
variableController = divVariableController,
)
)
@@ -9,6 +9,9 @@ import com.yandex.div.compose.DivComposeConfiguration
import com.yandex.div.compose.DivContext
import com.yandex.div.compose.DivReporter
import com.yandex.div.compose.lottie.LottieExtensionHandler
import com.yandex.div.compose.video.viewbased.ViewBasedDivVideoPlayerFactory
import com.yandex.div.core.annotations.ExperimentalApi
import com.yandex.div.video.m3.ExoDivPlayerFactory
import com.yandex.divkit.demo.div.ChronometerViewFactory
import com.yandex.divkit.demo.div.CustomContainerViewFactory
import com.yandex.divkit.demo.div.CustomTextViewFactory
@@ -17,6 +20,7 @@ import com.yandex.divkit.demo.font.ComposeFontSourceProvider
import com.yandex.div.compose.DivView as ComposeDivView
import com.yandex.divkit.demo.screenshot.DivAssetReader
@OptIn(ExperimentalApi::class)
class RegressionComposeViewCreator(context: Context) {
private val assetReader = DivAssetReader(context)
@@ -47,6 +51,7 @@ class RegressionComposeViewCreator(context: Context) {
)
),
fontSourceProvider = ComposeFontSourceProvider(),
playerFactory = ViewBasedDivVideoPlayerFactory(ExoDivPlayerFactory(activity)),
reporter = reporter
)
)
@@ -11,7 +11,10 @@ import com.yandex.div.compose.DivContext
import com.yandex.div.compose.DivView
import com.yandex.div.compose.internal.DivDebugConfiguration
import com.yandex.div.compose.internal.ImageLoaderProvider
import com.yandex.div.compose.video.viewbased.ViewBasedDivVideoPlayerFactory
import com.yandex.div.core.annotations.ExperimentalApi
import com.yandex.div.core.annotations.InternalApi
import com.yandex.div.video.m3.ExoDivPlayerFactory
import com.yandex.div.data.DivParsingEnvironment
import com.yandex.div.json.ParsingErrorLogger
import com.yandex.div2.DivAction
@@ -25,7 +28,7 @@ import org.json.JSONObject
adb shell am start -n com.yandex.divkit.demo/com.yandex.divkit.demo.screenshot.DivComposeScreenshotActivity \
-e DivComposeScreenshotActivity.EXTRA_DIV_ASSET_NAME snapshot_test_data/div-text/all_attributes.json
*/
@OptIn(InternalApi::class)
@OptIn(InternalApi::class, ExperimentalApi::class)
class DivComposeScreenshotActivity : ComponentActivity() {
private lateinit var divContext: DivContext
@@ -46,7 +49,8 @@ class DivComposeScreenshotActivity : ComponentActivity() {
divContext = DivContext(
baseContext = this,
configuration = DivComposeConfiguration(
fontSourceProvider = ComposeFontSourceProvider()
fontSourceProvider = ComposeFontSourceProvider(),
playerFactory = ViewBasedDivVideoPlayerFactory(ExoDivPlayerFactory(this))
),
debugConfiguration = DivDebugConfiguration(
imageLoaderProvider = imageLoaderProvider
@@ -271,6 +271,8 @@ class DivComposeScreenshotTest(case: String, escapedCase: String) {
// div-grid
"snapshot_test_data/div-grid",
"snapshot_test_data/div-grid/examples",
// div-video
"snapshot_test_data/div-video",
).expandDirectories().withEscapedParameter()
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

+1
View File
@@ -28,6 +28,7 @@ include ':coil'
include ':compose'
include ':compose-interop'
include ':compose-lottie'
include ':compose-video-view-based'
include ':div'
include ':divkit-benchmark-app'
include ':div-core'
@@ -0,0 +1,63 @@
package com.yandex.div.test.data
import android.net.Uri
import androidx.core.net.toUri
import com.yandex.div.json.expressions.Expression
import com.yandex.div2.Div
import com.yandex.div2.DivAction
import com.yandex.div2.DivVideo
import com.yandex.div2.DivVideoScale
import com.yandex.div2.DivVideoSource
import org.json.JSONObject
fun video(
autostart: Expression<Boolean> = constant(false),
bufferingActions: List<DivAction>? = null,
elapsedTimeVariable: String? = null,
endActions: List<DivAction>? = null,
fatalActions: List<DivAction>? = null,
id: String? = null,
muted: Expression<Boolean> = constant(false),
pauseActions: List<DivAction>? = null,
playbackSpeed: Expression<Double> = constant(1.0),
playerSettingsPayload: Expression<JSONObject>? = null,
preview: Expression<String>? = null,
repeatable: Expression<Boolean> = constant(false),
resumeActions: List<DivAction>? = null,
scale: Expression<DivVideoScale> = constant(DivVideoScale.FIT),
videoSources: List<DivVideoSource>? = null,
): Div {
return Div.Video(
value = DivVideo(
autostart = autostart,
bufferingActions = bufferingActions,
elapsedTimeVariable = elapsedTimeVariable,
endActions = endActions,
fatalActions = fatalActions,
id = id,
muted = muted,
pauseActions = pauseActions,
playbackSpeed = playbackSpeed,
playerSettingsPayload = playerSettingsPayload,
preview = preview,
repeatable = repeatable,
resumeActions = resumeActions,
scale = scale,
videoSources = videoSources,
)
)
}
fun videoSource(
url: Expression<Uri> = constant("empty://".toUri()),
mimeType: Expression<String> = constant("video/mp4"),
bitrate: Expression<Long>? = null,
resolution: DivVideoSource.Resolution? = null,
): DivVideoSource {
return DivVideoSource(
bitrate = bitrate,
mimeType = mimeType,
resolution = resolution,
url = url,
)
}