diff --git a/.mapping.json b/.mapping.json index 6b73534eb..54f033314 100644 --- a/.mapping.json +++ b/.mapping.json @@ -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", diff --git a/client/android/build.gradle b/client/android/build.gradle index b8ba4ed69..5a5ab57b3 100644 --- a/client/android/build.gradle +++ b/client/android/build.gradle @@ -113,6 +113,7 @@ apiValidation { "api-generator-test", "compose", "compose-lottie", + "compose-video-view-based", "divkit-benchmark-app", "divkit-demo-app", "divkit-perftests", diff --git a/client/android/compose-video-view-based/build.gradle.kts b/client/android/compose-video-view-based/build.gradle.kts new file mode 100644 index 000000000..e58e326e2 --- /dev/null +++ b/client/android/compose-video-view-based/build.gradle.kts @@ -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) +} diff --git a/client/android/compose-video-view-based/src/main/kotlin/com/yandex/div/compose/video/viewbased/ViewBasedDivVideoPlayer.kt b/client/android/compose-video-view-based/src/main/kotlin/com/yandex/div/compose/video/viewbased/ViewBasedDivVideoPlayer.kt new file mode 100644 index 000000000..5d91136bf --- /dev/null +++ b/client/android/compose-video-view-based/src/main/kotlin/com/yandex/div/compose/video/viewbased/ViewBasedDivVideoPlayer.kt @@ -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 = _isReady + + private val _isPlaying = MutableStateFlow(false) + override val isPlaying: StateFlow = _isPlaying + + private val _isBuffering = MutableStateFlow(false) + override val isBuffering: StateFlow = _isBuffering + + private val _isEnded = MutableStateFlow(false) + override val isEnded: StateFlow = _isEnded + + private val _error = MutableStateFlow(null) + override val error: StateFlow = _error + + private val _currentTimeMs = MutableStateFlow(0L) + override val currentTimeMs: StateFlow = _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.toPlayerSources(): List = 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, + ) diff --git a/client/android/compose-video-view-based/src/main/kotlin/com/yandex/div/compose/video/viewbased/ViewBasedDivVideoPlayerFactory.kt b/client/android/compose-video-view-based/src/main/kotlin/com/yandex/div/compose/video/viewbased/ViewBasedDivVideoPlayerFactory.kt new file mode 100644 index 000000000..996c768f5 --- /dev/null +++ b/client/android/compose-video-view-based/src/main/kotlin/com/yandex/div/compose/video/viewbased/ViewBasedDivVideoPlayerFactory.kt @@ -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) +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivComposeConfiguration.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivComposeConfiguration.kt index 467bb8a48..f89ed91e7 100644 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivComposeConfiguration.kt +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivComposeConfiguration.kt @@ -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 = MutableStateFlow(false) + override val isPlaying: StateFlow = MutableStateFlow(false) + override val isBuffering: StateFlow = MutableStateFlow(false) + override val isEnded: StateFlow = MutableStateFlow(false) + override val error: StateFlow = MutableStateFlow(null) + override val currentTimeMs: StateFlow = 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 + } +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivActionSource.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivActionSource.kt index f374f2227..7590434b4 100644 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivActionSource.kt +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivActionSource.kt @@ -13,5 +13,6 @@ enum class DivActionSource { LONG_TAP, TAP, TRIGGER, + VIDEO, VISIBILITY } diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextComponent.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextComponent.kt index 92bb22382..b72fb1480 100644 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextComponent.kt +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextComponent.kt @@ -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) diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/variables/DivVariableMutableState.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/variables/DivVariableMutableState.kt index 3d4b5b228..c0a2b3b24 100644 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/variables/DivVariableMutableState.kt +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/variables/DivVariableMutableState.kt @@ -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 { + return mutableStateFromIntegerVariable(variableName) ?: remember { mutableStateOf(defaultValue) } +} + @Composable internal fun mutableStateFromStringVariable(variableName: String): MutableState? { return rememberVariableMutableState( @@ -44,6 +49,18 @@ internal fun mutableStateFromBooleanVariable(variableName: String): MutableState ) } +@Composable +internal fun mutableStateFromIntegerVariable(variableName: String): MutableState? { + 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 rememberVariableMutableState( variableName: String, diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayer.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayer.kt new file mode 100644 index 000000000..b6948aba0 --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayer.kt @@ -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 + + /** + * `true` while the player is actively playing. + */ + val isPlaying: StateFlow + + /** + * `true` while the player is buffering. + */ + val isBuffering: StateFlow + + /** + * `true` when playback has reached the end. + */ + val isEnded: StateFlow + + /** + * Last fatal error, or `null` if no error has occurred. + */ + val error: StateFlow + + /** + * Current playback position in milliseconds; updated by the player as playback + * progresses. Use [seek] to change the position. + */ + val currentTimeMs: StateFlow + + 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) +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayerConfig.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayerConfig.kt new file mode 100644 index 000000000..00c3fdbdd --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayerConfig.kt @@ -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 = 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, +) diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayerFactory.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayerFactory.kt new file mode 100644 index 000000000..1fcf8f07b --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/video/DivVideoPlayerFactory.kt @@ -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 +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt index 3da0d3ea0..11ab1fb30 100644 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt @@ -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") } } diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/video/DivVideoView.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/video/DivVideoView.kt new file mode 100644 index 000000000..5aae5cf7a --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/video/DivVideoView.kt @@ -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) +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/video/VideoActionPerformer.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/video/VideoActionPerformer.kt new file mode 100644 index 000000000..a5451779b --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/video/VideoActionPerformer.kt @@ -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?) { + if (actions.isNullOrEmpty()) return + actionHandler.handle(actionHandlingContext, actions, source = DivActionSource.VIDEO) + } +} diff --git a/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithVideoTest.kt b/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithVideoTest.kt new file mode 100644 index 000000000..b59270f85 --- /dev/null +++ b/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithVideoTest.kt @@ -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(null) + + override val isReady: StateFlow = isReadyState + override val isPlaying: StateFlow = isPlayingState + override val isBuffering: StateFlow = isBufferingState + override val isEnded: StateFlow = isEndedState + override val currentTimeMs: StateFlow = currentTimeMsState + override val error: StateFlow = 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 + } +} diff --git a/client/android/divkit-demo-app/build.gradle b/client/android/divkit-demo-app/build.gradle index 5082bec04..75a932abb 100644 --- a/client/android/divkit-demo-app/build.gradle +++ b/client/android/divkit-demo-app/build.gradle @@ -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') diff --git a/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoRendererFacade.kt b/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoRendererFacade.kt index ca0d50c1c..b0c050494 100644 --- a/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoRendererFacade.kt +++ b/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoRendererFacade.kt @@ -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, ) ) diff --git a/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/regression/RegressionComposeViewCreator.kt b/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/regression/RegressionComposeViewCreator.kt index c88687406..1767758ee 100644 --- a/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/regression/RegressionComposeViewCreator.kt +++ b/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/regression/RegressionComposeViewCreator.kt @@ -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 ) ) diff --git a/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/screenshot/DivComposeScreenshotActivity.kt b/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/screenshot/DivComposeScreenshotActivity.kt index 9b51e24b0..0e423eaac 100644 --- a/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/screenshot/DivComposeScreenshotActivity.kt +++ b/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/screenshot/DivComposeScreenshotActivity.kt @@ -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 diff --git a/client/android/divkit-demo-app/src/screenshotTest/java/com/yandex/div/DivComposeScreenshotTest.kt b/client/android/divkit-demo-app/src/screenshotTest/java/com/yandex/div/DivComposeScreenshotTest.kt index b65259dc1..56d60a437 100644 --- a/client/android/divkit-demo-app/src/screenshotTest/java/com/yandex/div/DivComposeScreenshotTest.kt +++ b/client/android/divkit-demo-app/src/screenshotTest/java/com/yandex/div/DivComposeScreenshotTest.kt @@ -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() } diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png index 23b2f6bb9..48faf0fa9 100644 Binary files a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png new file mode 100644 index 000000000..9360c9ef1 Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png new file mode 100644 index 000000000..35ebdedde Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png new file mode 100644 index 000000000..7fd02aea3 Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png index 351da93d8..76993d677 100644 Binary files a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png new file mode 100644 index 000000000..c2a1f8544 Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png new file mode 100644 index 000000000..581253c2f Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png new file mode 100644 index 000000000..a475643e1 Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png index a8a106510..a83278e91 100644 Binary files a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png new file mode 100644 index 000000000..cb2c29cc4 Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png new file mode 100644 index 000000000..abba1f830 Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png new file mode 100644 index 000000000..7fd02aea3 Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png index 351da93d8..76993d677 100644 Binary files a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png new file mode 100644 index 000000000..c2a1f8544 Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png new file mode 100644 index 000000000..581253c2f Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png new file mode 100644 index 000000000..a475643e1 Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png index 2de76ac45..fdf777eda 100644 Binary files a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png new file mode 100644 index 000000000..67fa3ba8c Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png new file mode 100644 index 000000000..924512849 Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png new file mode 100644 index 000000000..2b37609ba Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png index 2898268b8..38ed62184 100644 Binary files a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png new file mode 100644 index 000000000..e02161d68 Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png new file mode 100644 index 000000000..6a6a27e5b Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png new file mode 100644 index 000000000..bad320034 Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png index 2de76ac45..fdf777eda 100644 Binary files a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png new file mode 100644 index 000000000..67fa3ba8c Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png new file mode 100644 index 000000000..924512849 Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png new file mode 100644 index 000000000..2b37609ba Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png index 9fa161b3a..b35df385c 100644 Binary files a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-container/baseline-with-images.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png new file mode 100644 index 000000000..0a945c3dc Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fill.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png new file mode 100644 index 000000000..e4e8a3e3d Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-fit.png differ diff --git a/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png new file mode 100644 index 000000000..a965fbb89 Binary files /dev/null and b/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.DivComposeScreenshotTest/div-video/video-preview-scale-no-scale.png differ diff --git a/client/android/settings.gradle b/client/android/settings.gradle index bb4d70126..efb02d03d 100644 --- a/client/android/settings.gradle +++ b/client/android/settings.gradle @@ -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' diff --git a/client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivVideoUtils.kt b/client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivVideoUtils.kt new file mode 100644 index 000000000..63de2e8be --- /dev/null +++ b/client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivVideoUtils.kt @@ -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 = constant(false), + bufferingActions: List? = null, + elapsedTimeVariable: String? = null, + endActions: List? = null, + fatalActions: List? = null, + id: String? = null, + muted: Expression = constant(false), + pauseActions: List? = null, + playbackSpeed: Expression = constant(1.0), + playerSettingsPayload: Expression? = null, + preview: Expression? = null, + repeatable: Expression = constant(false), + resumeActions: List? = null, + scale: Expression = constant(DivVideoScale.FIT), + videoSources: List? = 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 = constant("empty://".toUri()), + mimeType: Expression = constant("video/mp4"), + bitrate: Expression? = null, + resolution: DivVideoSource.Resolution? = null, +): DivVideoSource { + return DivVideoSource( + bitrate = bitrate, + mimeType = mimeType, + resolution = resolution, + url = url, + ) +}