diff --git a/android/src/main/java/com/magicleap/magicscript/ARComponentManager.java b/android/src/main/java/com/magicleap/magicscript/ARComponentManager.java index b224e1b9..12be3597 100644 --- a/android/src/main/java/com/magicleap/magicscript/ARComponentManager.java +++ b/android/src/main/java/com/magicleap/magicscript/ARComponentManager.java @@ -78,11 +78,11 @@ import com.magicleap.magicscript.scene.nodes.UiTextEditNode; import com.magicleap.magicscript.scene.nodes.UiTextNode; import com.magicleap.magicscript.scene.nodes.UiTimePickerNode; -import com.magicleap.magicscript.scene.nodes.audio.AudioFileProvider; +import com.magicleap.magicscript.scene.nodes.audio.FileProvider; import com.magicleap.magicscript.scene.nodes.audio.AudioNode; import com.magicleap.magicscript.scene.nodes.audio.ExternalAudioEngine; import com.magicleap.magicscript.scene.nodes.audio.GvrAudioEngineWrapper; -import com.magicleap.magicscript.scene.nodes.audio.UriAudioProvider; +import com.magicleap.magicscript.scene.nodes.audio.UriFileProvider; import com.magicleap.magicscript.scene.nodes.audio.VrAudioEngine; import com.magicleap.magicscript.scene.nodes.base.ReactNode; import com.magicleap.magicscript.scene.nodes.button.UiButtonNode; @@ -263,8 +263,9 @@ public void createModelNode(final ReadableMap props, final String nodeId) { @ReactMethod public void createVideoNode(final ReadableMap props, final String nodeId) { mainHandler.post(() -> { - VideoPlayer videoPlayer = new VideoPlayerImpl(context); - addNode(new VideoNode(props, context, videoPlayer, videoRenderableLoader, videoNodeClipper, arResourcesProvider), nodeId); + FileProvider fileProvider = new UriFileProvider(context); + VideoPlayer videoPlayer = new VideoPlayerImpl(context, fileProvider); + addNode(new VideoNode(props, context, videoPlayer, videoRenderableLoader, viewRenderableLoader, videoNodeClipper, fontProvider, arResourcesProvider), nodeId); }); } @@ -452,8 +453,8 @@ public void createAudioNode(final ReadableMap props, final String nodeId) { GvrAudioEngine gvrAudioEngine = new GvrAudioEngine(context, GvrAudioEngine.RenderingMode.BINAURAL_HIGH_QUALITY); ExternalAudioEngine externalAudioEngine = new GvrAudioEngineWrapper(gvrAudioEngine); VrAudioEngine audioEngine = new VrAudioEngine(Executors.newSingleThreadExecutor(), externalAudioEngine); - AudioFileProvider audioFileProvider = new UriAudioProvider(context); - AudioNode node = new AudioNode(props, context, audioEngine, audioFileProvider); + FileProvider fileProvider = new UriFileProvider(context); + AudioNode node = new AudioNode(props, context, audioEngine, fileProvider); addNode(node, nodeId); }); } diff --git a/android/src/main/java/com/magicleap/magicscript/scene/nodes/UiTextNode.kt b/android/src/main/java/com/magicleap/magicscript/scene/nodes/UiTextNode.kt index 296d03d1..e366f5a6 100644 --- a/android/src/main/java/com/magicleap/magicscript/scene/nodes/UiTextNode.kt +++ b/android/src/main/java/com/magicleap/magicscript/scene/nodes/UiTextNode.kt @@ -158,6 +158,9 @@ open class UiTextNode( "right" -> { (view as TextView).gravity = Gravity.RIGHT } + "bottom-center" -> { + (view as TextView).gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL + } } } diff --git a/android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/AudioNode.kt b/android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/AudioNode.kt index 9fe5a57c..7627e698 100644 --- a/android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/AudioNode.kt +++ b/android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/AudioNode.kt @@ -30,7 +30,7 @@ open class AudioNode( initProps: ReadableMap, private val context: Context, private var audioEngine: AudioEngine, - private val fileProvider: AudioFileProvider + private val fileProvider: FileProvider ) : TransformNode(initProps, false) { companion object { diff --git a/android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/AudioFileProvider.kt b/android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/FileProvider.kt similarity index 96% rename from android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/AudioFileProvider.kt rename to android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/FileProvider.kt index 3fe71c8a..63936de1 100644 --- a/android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/AudioFileProvider.kt +++ b/android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/FileProvider.kt @@ -19,7 +19,7 @@ package com.magicleap.magicscript.scene.nodes.audio import android.net.Uri import java.io.File -interface AudioFileProvider { +interface FileProvider { fun provideFile(uri: Uri, result: (File) -> Unit) fun onDestroy() } diff --git a/android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/UriAudioProvider.kt b/android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/UriFileProvider.kt similarity index 91% rename from android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/UriAudioProvider.kt rename to android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/UriFileProvider.kt index e4dba9f6..b62e8814 100644 --- a/android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/UriAudioProvider.kt +++ b/android/src/main/java/com/magicleap/magicscript/scene/nodes/audio/UriFileProvider.kt @@ -24,9 +24,12 @@ import java.io.BufferedInputStream import java.io.File import java.io.InputStream import java.net.URL - -class UriAudioProvider(private val context: Context) : - AudioFileProvider { +/* + * When the Uri is provided and framework is not handling the Files properly, + * we use this provider to fetch the files and use it for example for audio files, subtitle files etc + */ +class UriFileProvider(private val context: Context) : + FileProvider { companion object { private const val READ_TIMEOUT = 5000 @@ -64,7 +67,7 @@ class UriAudioProvider(private val context: Context) : bufferedInputStream.close() fileDownloaded(uri.path, result, file) } catch (e: Exception) { - logMessage("Error during reading Audio File $e", true) + logMessage("Error during reading file $e", true) } } ) diff --git a/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/UiSubtitlesNode.kt b/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/UiSubtitlesNode.kt new file mode 100644 index 00000000..efe7549c --- /dev/null +++ b/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/UiSubtitlesNode.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 Magic Leap, Inc. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.magicleap.magicscript.scene.nodes.video + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import com.facebook.react.bridge.ReadableMap +import com.google.ar.sceneform.rendering.Renderable +import com.magicleap.magicscript.R +import com.magicleap.magicscript.ar.RenderPriority +import com.magicleap.magicscript.ar.clip.Clipper +import com.magicleap.magicscript.ar.renderable.ViewRenderableLoader +import com.magicleap.magicscript.font.FontProvider +import com.magicleap.magicscript.scene.nodes.UiTextNode + +class UiSubtitlesNode( + props: ReadableMap, + context: Context, + viewRenderableLoader: ViewRenderableLoader, + nodeClipper: Clipper, + fontProvider: FontProvider +) : UiTextNode(props, context, viewRenderableLoader, nodeClipper, fontProvider) { + + override fun provideView(context: Context): View { + return LayoutInflater.from(context).inflate(R.layout.subtitles, null) as TextView + } + + override fun onViewLoaded(viewRenderable: Renderable) { + super.onViewLoaded(viewRenderable) + viewRenderable.renderPriority = RenderPriority.ABOVE_DEFAULT + } +} diff --git a/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/VideoNode.kt b/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/VideoNode.kt index e59a2302..b3a2adaa 100644 --- a/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/VideoNode.kt +++ b/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/VideoNode.kt @@ -17,7 +17,10 @@ package com.magicleap.magicscript.scene.nodes.video import android.content.Context +import android.net.Uri import android.os.Bundle +import com.facebook.react.bridge.JavaOnlyArray +import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.ReadableMap import com.google.ar.sceneform.math.Vector3 import com.google.ar.sceneform.rendering.ExternalTexture @@ -27,19 +30,26 @@ import com.magicleap.magicscript.ar.clip.Clipper import com.magicleap.magicscript.ar.renderable.RenderableLoadRequest import com.magicleap.magicscript.ar.renderable.RenderableResult import com.magicleap.magicscript.ar.renderable.VideoRenderableLoader +import com.magicleap.magicscript.ar.renderable.ViewRenderableLoader +import com.magicleap.magicscript.font.FontProvider +import com.magicleap.magicscript.scene.NodesManager +import com.magicleap.magicscript.scene.nodes.UiTextNode +import com.magicleap.magicscript.scene.nodes.UiTextNode.Companion.PROP_BOUNDS_SIZE +import com.magicleap.magicscript.scene.nodes.UiTextNode.Companion.PROP_TEXT_ALIGNMENT import com.magicleap.magicscript.scene.nodes.base.TransformNode import com.magicleap.magicscript.scene.nodes.props.AABB -import com.magicleap.magicscript.utils.logMessage -import com.magicleap.magicscript.utils.putDefault -import com.magicleap.magicscript.utils.readFilePath +import com.magicleap.magicscript.utils.* class VideoNode( initProps: ReadableMap, private val context: Context, private val videoPlayer: VideoPlayer, private val videoRenderableLoader: VideoRenderableLoader, + private val viewRenderableLoader: ViewRenderableLoader, private val nodeClipper: Clipper, + private val fontProvider: FontProvider, private val arResourcesProvider: ArResourcesProvider + ) : TransformNode(initProps, useContentNodeAlignment = true) { companion object { @@ -50,12 +60,16 @@ class VideoNode( const val PROP_LOOPING = "looping" const val PROP_ACTION = "action" const val PROP_VOLUME = "volume" + const val PROP_TIMED_TEXT_PATH = "timedTextPath" + const val PROP_SEEK_TO = "seekTo" const val ACTION_START = "start" const val ACTION_STOP = "stop" const val ACTION_PAUSE = "pause" const val DEFAULT_VOLUME = 1.0 + + private const val SUBTITLES_MARGIN_BOTTOM = 0.05 // in meters } var onVideoPreparedListener: (() -> Unit)? = null @@ -75,6 +89,7 @@ class VideoNode( private val initialHeight = 1F // meters private var lastUserAction: String = "" + private var subtitles: UiSubtitlesNode? = null init { // set default values of properties @@ -91,6 +106,11 @@ class VideoNode( setAction(props) setLooping(props) setVolume(props) + setSeekTo(props) + + if (props.containsAny(PROP_SIZE)) { + updateSubtitlesProps() + } } override fun onVisibilityChanged(visibility: Boolean) { @@ -133,6 +153,10 @@ class VideoNode( // destroying media player when node is detached (e.g. on scene change) override fun onDestroy() { super.onDestroy() + if (subtitles != null) { + contentNode.removeChild(subtitles) + subtitles = null + } videoPlayer.release() renderableLoadRequest?.let { videoRenderableLoader.cancel(it) @@ -143,6 +167,58 @@ class VideoNode( nodeClipper.applyClipBounds(this, clipBounds) } + private fun createSubtitles() { + if (subtitles == null) { + subtitles = UiSubtitlesNode( + props = getSubtitleProps(), + context = context, + viewRenderableLoader = viewRenderableLoader, + nodeClipper = nodeClipper, + fontProvider = fontProvider + ) + contentNode.addChild(subtitles) + subtitles?.build() + } + } + + private fun getSubtitleProps(): JavaOnlyMap { + val videoSize = readVideoSize(properties) ?: Vector2(initialWidth, initialHeight) + + // we have to apply reverted content scale, so the subtitles are not stretched + val contentScale = contentNode.localScale + val scaleX = if (contentScale.x > 0) 1f / contentScale.x else 0f + val scaleY = if (contentScale.y > 0) 1f / contentScale.y else 0f + val scaleZ = if (contentScale.x > 0) 1f / contentScale.z else 0f + + return JavaOnlyMap.of( + UiTextNode.PROP_TEXT_SIZE, 0.1, + PROP_LOCAL_POSITION, JavaOnlyArray.of(0f, SUBTITLES_MARGIN_BOTTOM, 0.01f), + PROP_LOCAL_SCALE, JavaOnlyArray.of(scaleX, scaleY, scaleZ), + PROP_ALIGNMENT, "bottom-center", + PROP_TEXT_ALIGNMENT, "bottom-center", + PROP_BOUNDS_SIZE, JavaOnlyMap.of( + PROP_BOUNDS_SIZE, JavaOnlyArray.of(videoSize.x, videoSize.y) + ) + ) + } + + private fun updateSubtitlesProps() { + subtitles?.update(getSubtitleProps()) + } + + private fun getTimedTextPath(props: Bundle): Uri? { + if (props.containsKey(PROP_TIMED_TEXT_PATH)) { + return props.readFilePath(PROP_TIMED_TEXT_PATH, context) + } + return null + } + + private fun setSeekTo(props: Bundle) { + if (props.containsKey(PROP_SEEK_TO)) { + videoPlayer.seekTo(props.getDouble(PROP_SEEK_TO).toInt()) + } + } + private fun loadVideo() { if (!arResourcesProvider.isArLoaded()) { // ar must be load to create ExternalTexture object @@ -151,11 +227,38 @@ class VideoNode( val videoUri = properties.readFilePath(PROP_VIDEO_PATH, context) if (videoUri != null) { + subtitles?.update( + // add empty text to hide last subtitles + JavaOnlyMap.of(UiTextNode.PROP_TEXT, "") + ) val texture = ExternalTexture() try { - videoPlayer.loadVideo(videoUri, texture.surface, onLoadedListener = { - onVideoPreparedListener?.invoke() - }) + val timedTextUri = getTimedTextPath(properties) + if (timedTextUri != null) { + videoPlayer.loadVideo( + uri = videoUri, + subtitlesPath = timedTextUri, + onSubtitleChangeListener = { + subtitles?.update(JavaOnlyMap.of(UiTextNode.PROP_TEXT, it)) + }, + surface = texture.surface, + onLoadedListener = { + onVideoPreparedListener?.invoke() + createSubtitles() + }) + } else { + if (subtitles != null) { + contentNode.removeChild(subtitles) + subtitles = null + } + videoPlayer.loadVideo( + uri = videoUri, + surface = texture.surface, + onLoadedListener = { + onVideoPreparedListener?.invoke() + }) + } + } catch (exception: Exception) { logMessage("video load exception: $exception", warn = true) } @@ -183,16 +286,22 @@ class VideoNode( // changing video size by scaling the content node private fun setSize(props: Bundle) { + readVideoSize(props)?.let { videoSize -> + val scaleX = videoSize.x / initialWidth + val scaleY = videoSize.y / initialHeight + contentNode.localScale = Vector3(scaleX, scaleY, 1F) + applyClipBounds() + } + } + + private fun readVideoSize(props: Bundle): Vector2? { val sizeArray = props.getSerializable(PROP_SIZE) as? ArrayList if (sizeArray != null && sizeArray.size == 2) { val widthMeters = sizeArray[0].toFloat() val heightMeters = sizeArray[1].toFloat() - - val scaleX = widthMeters / initialWidth - val scaleY = heightMeters / initialHeight - contentNode.localScale = Vector3(scaleX, scaleY, 1F) - applyClipBounds() + return Vector2(widthMeters, heightMeters) } + return null } private fun setAction(props: Bundle) { diff --git a/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/VideoPlayer.kt b/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/VideoPlayer.kt index 24e55f42..b33b092a 100644 --- a/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/VideoPlayer.kt +++ b/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/VideoPlayer.kt @@ -29,7 +29,7 @@ interface VideoPlayer { val isReady: Boolean @Throws(Exception::class) - fun loadVideo(uri: Uri, surface: Surface, onLoadedListener: () -> Unit) + fun loadVideo(uri: Uri, subtitlesPath: Uri? = null, onSubtitleChangeListener: ((String) -> Unit)? = null, surface: Surface, onLoadedListener: () -> Unit) @Throws(IllegalStateException::class) fun start() @@ -41,4 +41,8 @@ interface VideoPlayer { fun stop() fun release() + + fun seekTo(millis: Int) + + fun clearTimedTextListener() } \ No newline at end of file diff --git a/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/VideoPlayerImpl.kt b/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/VideoPlayerImpl.kt index 7ab0d281..36ed7582 100644 --- a/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/VideoPlayerImpl.kt +++ b/android/src/main/java/com/magicleap/magicscript/scene/nodes/video/VideoPlayerImpl.kt @@ -17,13 +17,25 @@ package com.magicleap.magicscript.scene.nodes.video import android.content.Context +import android.media.MediaFormat.MIMETYPE_TEXT_SUBRIP import android.media.MediaPlayer +import android.media.MediaPlayer.MEDIA_MIMETYPE_TEXT_SUBRIP +import android.media.MediaPlayer.TrackInfo import android.net.Uri +import android.os.Build +import android.os.Handler +import android.os.Looper import android.view.Surface +import com.magicleap.magicscript.scene.nodes.audio.FileProvider +import com.magicleap.magicscript.utils.logMessage -class VideoPlayerImpl(private val context: Context) : VideoPlayer, MediaPlayer.OnPreparedListener { - private val mediaPlayer = GlobalMediaPlayerPool.createMediaPlayer() +class VideoPlayerImpl( + private val context: Context, + private val fileProvider: FileProvider +) : VideoPlayer, MediaPlayer.OnPreparedListener { + + private var mediaPlayer = GlobalMediaPlayerPool.createMediaPlayer() private var onLoadedListener: (() -> Unit)? = null private var ready = false @@ -54,9 +66,10 @@ class VideoPlayerImpl(private val context: Context) : VideoPlayer, MediaPlayer.O } @Throws(Exception::class) - override fun loadVideo(uri: Uri, surface: Surface, onLoadedListener: () -> Unit) { + override fun loadVideo(uri: Uri, subtitlesPath: Uri?, onSubtitleChangeListener: ((String) -> Unit)?, surface: Surface, onLoadedListener: () -> Unit) { ready = false - mediaPlayer.reset() + mediaPlayer.release() // We have to release media player and create new one to add new subtitles + mediaPlayer = GlobalMediaPlayerPool.createMediaPlayer() val path = uri.toString() if (path.startsWith("http")) { mediaPlayer.setDataSource(path) // load from URL @@ -66,6 +79,23 @@ class VideoPlayerImpl(private val context: Context) : VideoPlayer, MediaPlayer.O } this.onLoadedListener = onLoadedListener mediaPlayer.setSurface(surface) + if (subtitlesPath == null) { + prepareMediaPlayer() + } else { + addSubtitles(subtitlesPath, onSubtitleChangeListener) + } + } + + private fun addSubtitles(subtitlesPath: Uri, onSubtitleChangeListener: ((String) -> Unit)?) { + fileProvider.provideFile(subtitlesPath) { + Handler(Looper.getMainLooper()).post { + addTimedTextPath(Uri.fromFile(it), onSubtitleChangeListener) + } + prepareMediaPlayer() + } + } + + private fun prepareMediaPlayer() { mediaPlayer.setOnPreparedListener(this) mediaPlayer.prepareAsync() // loading the video asynchronously } @@ -90,4 +120,39 @@ class VideoPlayerImpl(private val context: Context) : VideoPlayer, MediaPlayer.O mediaPlayer.release() } + private fun addTimedTextPath(path: Uri, onTextChangedListener: ((String) -> Unit)?) { + try { + if (Build.VERSION.SDK_INT >= 28) { + mediaPlayer.addTimedTextSource(context, path, MIMETYPE_TEXT_SUBRIP) + } else { + mediaPlayer.addTimedTextSource(context, path, MEDIA_MIMETYPE_TEXT_SUBRIP) + } + val textTrackIndex: Int = findTrackIndexFor(mediaPlayer.trackInfo) + if (textTrackIndex >= 0) { + mediaPlayer.selectTrack(textTrackIndex) + } + mediaPlayer.setOnTimedTextListener { _, text -> + onTextChangedListener?.invoke(text.text) + } + } catch (e: java.lang.Exception) { + logMessage("Error occured on adding subtitles to mediaPlayer: $e", warn = true) + } + } + + private fun findTrackIndexFor(trackInfo: Array): Int { + for (i in trackInfo.indices) { + if (trackInfo[i].trackType == TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) { + return i + } + } + return -1 + } + + override fun seekTo(millis: Int) { + mediaPlayer.seekTo(millis) + } + + override fun clearTimedTextListener() { + mediaPlayer.setOnTimedTextListener(null) + } } \ No newline at end of file diff --git a/android/src/main/java/com/magicleap/magicscript/scene/nodes/views/OutlineTextView.kt b/android/src/main/java/com/magicleap/magicscript/scene/nodes/views/OutlineTextView.kt new file mode 100644 index 00000000..30e297ee --- /dev/null +++ b/android/src/main/java/com/magicleap/magicscript/scene/nodes/views/OutlineTextView.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020 Magic Leap, Inc. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.magicleap.magicscript.scene.nodes.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import android.widget.TextView +import java.lang.reflect.Field + + +class OutlineTextView(context: Context, attributeSet: AttributeSet): TextView(context, attributeSet) { + + private var colorField: Field? = null + private var textColor = 0 + private val outlineColor = Color.BLACK + + init { + try { + colorField = TextView::class.java.getDeclaredField("mCurTextColor") + colorField?.isAccessible = true + textColor = textColors.defaultColor + paint.strokeWidth = 4f + + } catch (e: NoSuchFieldException) { + e.printStackTrace() + colorField = null + } + } + + override fun setTextColor(color: Int) { + textColor = color + super.setTextColor(color) + } + + override fun onDraw(canvas: Canvas?) { + if (colorField != null) { + setColorField(outlineColor) + paint.style = Paint.Style.STROKE + super.onDraw(canvas) + setColorField(textColor) + paint.style = Paint.Style.FILL + } + super.onDraw(canvas) + } + + private fun setColorField(color: Int) { + try { + colorField?.setInt(this, color) + } catch (e: Exception) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/android/src/main/res/layout/subtitles.xml b/android/src/main/res/layout/subtitles.xml new file mode 100644 index 00000000..3ca862cc --- /dev/null +++ b/android/src/main/res/layout/subtitles.xml @@ -0,0 +1,27 @@ + + + + \ No newline at end of file diff --git a/android/src/test/java/com/magicleap/magicscript/scene/nodes/VideoNodeTest.kt b/android/src/test/java/com/magicleap/magicscript/scene/nodes/VideoNodeTest.kt index a202173a..d671fd04 100644 --- a/android/src/test/java/com/magicleap/magicscript/scene/nodes/VideoNodeTest.kt +++ b/android/src/test/java/com/magicleap/magicscript/scene/nodes/VideoNodeTest.kt @@ -23,6 +23,8 @@ import com.google.ar.sceneform.math.Vector3 import com.magicleap.magicscript.ar.ArResourcesProvider import com.magicleap.magicscript.ar.renderable.VideoRenderableLoader import com.magicleap.magicscript.ar.clip.Clipper +import com.magicleap.magicscript.ar.renderable.ViewRenderableLoader +import com.magicleap.magicscript.font.FontProvider import com.magicleap.magicscript.reactArrayOf import com.magicleap.magicscript.reactMapOf import com.magicleap.magicscript.scene.nodes.props.AABB @@ -46,6 +48,8 @@ class VideoNodeTest { private lateinit var context: Context private lateinit var videoPlayer: VideoPlayer private lateinit var videoReadableLoader: VideoRenderableLoader + private lateinit var viewRenderableLoader: ViewRenderableLoader + private lateinit var fontProvider: FontProvider private lateinit var clipper: Clipper private lateinit var arResourcesProvider: ArResourcesProvider @@ -56,6 +60,8 @@ class VideoNodeTest { this.videoReadableLoader = mock() this.clipper = mock() this.arResourcesProvider = mock() + this.viewRenderableLoader = mock() + this.fontProvider = mock() // we have to return false, since creating textures inside VideoNode requires ar is loaded, // else exception will be thrown @@ -69,7 +75,9 @@ class VideoNodeTest { videoPlayer = videoPlayer, videoRenderableLoader = videoReadableLoader, nodeClipper = clipper, - arResourcesProvider = arResourcesProvider + arResourcesProvider = arResourcesProvider, + viewRenderableLoader = viewRenderableLoader, + fontProvider = fontProvider ) videoNode.build() diff --git a/android/src/test/java/com/magicleap/magicscript/scene/nodes/audio/AudioNodeTest.kt b/android/src/test/java/com/magicleap/magicscript/scene/nodes/audio/AudioNodeTest.kt index a0c0211b..45b7199b 100644 --- a/android/src/test/java/com/magicleap/magicscript/scene/nodes/audio/AudioNodeTest.kt +++ b/android/src/test/java/com/magicleap/magicscript/scene/nodes/audio/AudioNodeTest.kt @@ -36,12 +36,12 @@ class AudioNodeTest { private val FILE_URL = "http://localhost:8081/assets/resources/bg_stereo.mp3" private val audioEngine: AudioEngine = mock() - private lateinit var audioProvider: AudioFileProvider + private lateinit var provider: FileProvider private lateinit var tested: AudioNode @Before fun setup() { - audioProvider = spy(object : AudioFileProvider { + provider = spy(object : FileProvider { override fun provideFile(uri: Uri, result: (File) -> Unit) { result.invoke(File("")) } @@ -53,7 +53,7 @@ class AudioNodeTest { initProps = reactMapOf(), context = ApplicationProvider.getApplicationContext(), audioEngine = audioEngine, - fileProvider = audioProvider + fileProvider = provider ) } @@ -81,7 +81,7 @@ class AudioNodeTest { .fileName(FILE_URL) ) - verify(audioProvider).provideFile(eq(Uri.parse(FILE_URL)), any()) + verify(provider).provideFile(eq(Uri.parse(FILE_URL)), any()) } @Test @@ -214,7 +214,7 @@ class AudioNodeTest { fun `onDestroy should destroy file downloader and audio engine`() { tested.onDestroy() - verify(audioProvider).onDestroy() + verify(provider).onDestroy() verify(audioEngine).onDestroy() } } diff --git a/components/platform/platform-factory.js b/components/platform/platform-factory.js index 771f8a60..ebb5726f 100644 --- a/components/platform/platform-factory.js +++ b/components/platform/platform-factory.js @@ -151,6 +151,7 @@ export class PlatformFactory extends NativeFactory { ...(properties.filePath ? { filePath: this._processAssetSource(properties.filePath) } : {}), ...(properties.videoPath ? { videoPath: this._processAssetSource(properties.videoPath) } : {}), ...(properties.fileName ? { fileName: this._processAssetSource(properties.fileName) } : {}), + ...(properties.timedTextPath ? { timedTextPath: this._processAssetSource(properties.timedTextPath) } : {}), ...(properties.id ? { id: modifiedId } : {}), }); }