@stepincto/expo-video 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +20 -0
- package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
- package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
- package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
- package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
- package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
- package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
- package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
- package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
- package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
- package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
- package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
- package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
- package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
- package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
- package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
- package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
- package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
- package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
- package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
- package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
- package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
- package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
- package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
- package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
- package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
- package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
- package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
- package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
- package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
- package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
- package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
- package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
- package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
- package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
- package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
- package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
- package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
- package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
- package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
- package/android/src/main/res/layout/surface_player_view.xml +7 -0
- package/android/src/main/res/layout/texture_player_view.xml +7 -0
- package/android/src/main/res/values/styles.xml +9 -0
- package/app.plugin.js +1 -0
- package/build/NativeVideoModule.d.ts +16 -0
- package/build/NativeVideoModule.d.ts.map +1 -0
- package/build/NativeVideoModule.js +3 -0
- package/build/NativeVideoModule.js.map +1 -0
- package/build/NativeVideoModule.web.d.ts +3 -0
- package/build/NativeVideoModule.web.d.ts.map +1 -0
- package/build/NativeVideoModule.web.js +2 -0
- package/build/NativeVideoModule.web.js.map +1 -0
- package/build/NativeVideoView.d.ts +4 -0
- package/build/NativeVideoView.d.ts.map +1 -0
- package/build/NativeVideoView.js +6 -0
- package/build/NativeVideoView.js.map +1 -0
- package/build/VideoModule.d.ts +38 -0
- package/build/VideoModule.d.ts.map +1 -0
- package/build/VideoModule.js +53 -0
- package/build/VideoModule.js.map +1 -0
- package/build/VideoPlayer.d.ts +15 -0
- package/build/VideoPlayer.d.ts.map +1 -0
- package/build/VideoPlayer.js +52 -0
- package/build/VideoPlayer.js.map +1 -0
- package/build/VideoPlayer.types.d.ts +532 -0
- package/build/VideoPlayer.types.d.ts.map +1 -0
- package/build/VideoPlayer.types.js +2 -0
- package/build/VideoPlayer.types.js.map +1 -0
- package/build/VideoPlayer.web.d.ts +75 -0
- package/build/VideoPlayer.web.d.ts.map +1 -0
- package/build/VideoPlayer.web.js +376 -0
- package/build/VideoPlayer.web.js.map +1 -0
- package/build/VideoPlayerEvents.types.d.ts +262 -0
- package/build/VideoPlayerEvents.types.d.ts.map +1 -0
- package/build/VideoPlayerEvents.types.js +2 -0
- package/build/VideoPlayerEvents.types.js.map +1 -0
- package/build/VideoThumbnail.d.ts +29 -0
- package/build/VideoThumbnail.d.ts.map +1 -0
- package/build/VideoThumbnail.js +3 -0
- package/build/VideoThumbnail.js.map +1 -0
- package/build/VideoView.d.ts +44 -0
- package/build/VideoView.d.ts.map +1 -0
- package/build/VideoView.js +76 -0
- package/build/VideoView.js.map +1 -0
- package/build/VideoView.types.d.ts +147 -0
- package/build/VideoView.types.d.ts.map +1 -0
- package/build/VideoView.types.js +2 -0
- package/build/VideoView.types.js.map +1 -0
- package/build/VideoView.web.d.ts +9 -0
- package/build/VideoView.web.d.ts.map +1 -0
- package/build/VideoView.web.js +180 -0
- package/build/VideoView.web.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/resolveAssetSource.d.ts +3 -0
- package/build/resolveAssetSource.d.ts.map +1 -0
- package/build/resolveAssetSource.js +3 -0
- package/build/resolveAssetSource.js.map +1 -0
- package/build/resolveAssetSource.web.d.ts +4 -0
- package/build/resolveAssetSource.web.d.ts.map +1 -0
- package/build/resolveAssetSource.web.js +16 -0
- package/build/resolveAssetSource.web.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Cache/CachableRequest.swift +44 -0
- package/ios/Cache/CachedResource.swift +97 -0
- package/ios/Cache/CachingHelpers.swift +92 -0
- package/ios/Cache/MediaFileHandle.swift +94 -0
- package/ios/Cache/MediaInfo.swift +147 -0
- package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
- package/ios/Cache/SynchronizedHashTable.swift +23 -0
- package/ios/Cache/VideoCacheManager.swift +338 -0
- package/ios/ContentKeyDelegate.swift +214 -0
- package/ios/ContentKeyManager.swift +21 -0
- package/ios/Enums/AudioMixingMode.swift +37 -0
- package/ios/Enums/ContentType.swift +12 -0
- package/ios/Enums/DRMType.swift +20 -0
- package/ios/Enums/PlayerStatus.swift +10 -0
- package/ios/Enums/VideoContentFit.swift +39 -0
- package/ios/ExpoVideo.podspec +29 -0
- package/ios/NowPlayingManager.swift +296 -0
- package/ios/Records/BufferOptions.swift +12 -0
- package/ios/Records/DRMOptions.swift +24 -0
- package/ios/Records/PlaybackError.swift +10 -0
- package/ios/Records/Tracks.swift +176 -0
- package/ios/Records/VideoEventPayloads.swift +76 -0
- package/ios/Records/VideoMetadata.swift +16 -0
- package/ios/Records/VideoSize.swift +15 -0
- package/ios/Records/VideoSource.swift +25 -0
- package/ios/Thumbnails/VideoThumbnail.swift +27 -0
- package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
- package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
- package/ios/VideoAsset.swift +123 -0
- package/ios/VideoExceptions.swift +53 -0
- package/ios/VideoItem.swift +11 -0
- package/ios/VideoManager.swift +140 -0
- package/ios/VideoModule.swift +383 -0
- package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
- package/ios/VideoPlayer.swift +435 -0
- package/ios/VideoPlayerAudioTracks.swift +72 -0
- package/ios/VideoPlayerItem.swift +97 -0
- package/ios/VideoPlayerObserver.swift +523 -0
- package/ios/VideoPlayerSubtitles.swift +71 -0
- package/ios/VideoSourceLoader.swift +89 -0
- package/ios/VideoSourceLoaderListener.swift +34 -0
- package/ios/VideoView.swift +224 -0
- package/package.json +59 -0
- package/plugin/build/tsconfig.tsbuildinfo +1 -0
- package/plugin/build/withExpoVideo.d.ts +7 -0
- package/plugin/build/withExpoVideo.js +38 -0
- package/src/NativeVideoModule.ts +20 -0
- package/src/NativeVideoModule.web.ts +1 -0
- package/src/NativeVideoView.ts +8 -0
- package/src/VideoModule.ts +59 -0
- package/src/VideoPlayer.tsx +67 -0
- package/src/VideoPlayer.types.ts +613 -0
- package/src/VideoPlayer.web.tsx +451 -0
- package/src/VideoPlayerEvents.types.ts +313 -0
- package/src/VideoThumbnail.ts +31 -0
- package/src/VideoView.tsx +86 -0
- package/src/VideoView.types.ts +165 -0
- package/src/VideoView.web.tsx +214 -0
- package/src/index.ts +46 -0
- package/src/resolveAssetSource.ts +2 -0
- package/src/resolveAssetSource.web.ts +17 -0
- package/src/ts-declarations/react-native-assets.d.ts +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package expo.modules.video.records
|
|
2
|
+
|
|
3
|
+
import androidx.media3.common.PlaybackException
|
|
4
|
+
import expo.modules.kotlin.records.Field
|
|
5
|
+
import expo.modules.kotlin.records.Record
|
|
6
|
+
import java.io.Serializable
|
|
7
|
+
|
|
8
|
+
class PlaybackError(
|
|
9
|
+
@Field var message: String? = null
|
|
10
|
+
) : Record, Serializable {
|
|
11
|
+
constructor(exception: PlaybackException) : this(errorMessageFromException(exception))
|
|
12
|
+
|
|
13
|
+
companion object {
|
|
14
|
+
private fun errorMessageFromException(exception: PlaybackException): String {
|
|
15
|
+
val reason = "${exception.localizedMessage} ${exception.cause?.localizedMessage ?: ""}"
|
|
16
|
+
return "A playback exception has occurred: $reason"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
package expo.modules.video.records
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.OptIn
|
|
4
|
+
import androidx.media3.common.Format
|
|
5
|
+
import androidx.media3.common.util.UnstableApi
|
|
6
|
+
import expo.modules.kotlin.records.Field
|
|
7
|
+
import expo.modules.kotlin.records.Record
|
|
8
|
+
import java.io.Serializable
|
|
9
|
+
import java.util.Locale
|
|
10
|
+
|
|
11
|
+
class SubtitleTrack(
|
|
12
|
+
@Field val id: String,
|
|
13
|
+
@Field val language: String?,
|
|
14
|
+
@Field val label: String?
|
|
15
|
+
) : Record, Serializable {
|
|
16
|
+
companion object {
|
|
17
|
+
fun fromFormat(format: Format?): SubtitleTrack? {
|
|
18
|
+
format ?: return null
|
|
19
|
+
val id = format.id ?: return null
|
|
20
|
+
val language = format.language ?: return null
|
|
21
|
+
val label = Locale(language).displayLanguage
|
|
22
|
+
|
|
23
|
+
return SubtitleTrack(
|
|
24
|
+
id = id,
|
|
25
|
+
language = language,
|
|
26
|
+
label = label
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class AudioTrack(
|
|
33
|
+
@Field val id: String,
|
|
34
|
+
@Field val language: String?,
|
|
35
|
+
@Field val label: String?
|
|
36
|
+
) : Record, Serializable {
|
|
37
|
+
companion object {
|
|
38
|
+
fun fromFormat(format: Format?): AudioTrack? {
|
|
39
|
+
format ?: return null
|
|
40
|
+
val id = format.id ?: return null
|
|
41
|
+
val language = format.language
|
|
42
|
+
val label = language?.let { Locale(it).displayLanguage } ?: "Unknown"
|
|
43
|
+
|
|
44
|
+
return AudioTrack(
|
|
45
|
+
id = id,
|
|
46
|
+
language = language,
|
|
47
|
+
label = label
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@OptIn(UnstableApi::class)
|
|
54
|
+
class VideoTrack(
|
|
55
|
+
@Field val id: String,
|
|
56
|
+
@Field val size: VideoSize,
|
|
57
|
+
@Field val mimeType: String?,
|
|
58
|
+
@Field val isSupported: Boolean = true,
|
|
59
|
+
@Field val bitrate: Int? = null,
|
|
60
|
+
@Field val frameRate: Float? = null,
|
|
61
|
+
var format: Format? = null
|
|
62
|
+
) : Record, Serializable {
|
|
63
|
+
companion object {
|
|
64
|
+
fun fromFormat(format: Format?, isSupported: Boolean): VideoTrack? {
|
|
65
|
+
val id = format?.id ?: return null
|
|
66
|
+
val size = VideoSize(format)
|
|
67
|
+
val mimeType = format.sampleMimeType
|
|
68
|
+
val bitrate = format.bitrate.takeIf { it != Format.NO_VALUE }
|
|
69
|
+
val frameRate = format.frameRate.takeIf { it != Format.NO_VALUE.toFloat() }
|
|
70
|
+
return VideoTrack(
|
|
71
|
+
id = id,
|
|
72
|
+
size = size,
|
|
73
|
+
mimeType = mimeType,
|
|
74
|
+
isSupported = isSupported,
|
|
75
|
+
bitrate = bitrate,
|
|
76
|
+
frameRate = frameRate,
|
|
77
|
+
format = format
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package expo.modules.video.records
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.records.Field
|
|
4
|
+
import expo.modules.kotlin.records.Record
|
|
5
|
+
import expo.modules.video.enums.PlayerStatus
|
|
6
|
+
import java.io.Serializable
|
|
7
|
+
|
|
8
|
+
interface VideoEventPayload : Record, Serializable
|
|
9
|
+
|
|
10
|
+
class StatusChangedEventPayload(
|
|
11
|
+
@Field val status: PlayerStatus,
|
|
12
|
+
@Field val oldStatus: PlayerStatus?,
|
|
13
|
+
@Field val error: PlaybackError?
|
|
14
|
+
) : VideoEventPayload
|
|
15
|
+
|
|
16
|
+
class IsPlayingEventPayload(
|
|
17
|
+
@Field val isPlaying: Boolean,
|
|
18
|
+
@Field val oldIsPlaying: Boolean?
|
|
19
|
+
) : VideoEventPayload
|
|
20
|
+
|
|
21
|
+
class VolumeChangedEventPayload(
|
|
22
|
+
@Field val volume: Float,
|
|
23
|
+
@Field val oldVolume: Float?
|
|
24
|
+
) : VideoEventPayload
|
|
25
|
+
|
|
26
|
+
class MutedChangedEventPayload(
|
|
27
|
+
@Field val muted: Boolean,
|
|
28
|
+
@Field val oldMuted: Boolean?
|
|
29
|
+
) : VideoEventPayload
|
|
30
|
+
|
|
31
|
+
class SourceChangedEventPayload(
|
|
32
|
+
@Field val source: VideoSource?,
|
|
33
|
+
@Field val oldSource: VideoSource?
|
|
34
|
+
) : VideoEventPayload
|
|
35
|
+
|
|
36
|
+
class PlaybackRateChangedEventPayload(
|
|
37
|
+
@Field val playbackRate: Float,
|
|
38
|
+
@Field val oldPlaybackRate: Float?
|
|
39
|
+
) : VideoEventPayload
|
|
40
|
+
|
|
41
|
+
class TimeUpdate(
|
|
42
|
+
@Field var currentTime: Double = .0,
|
|
43
|
+
@Field var currentOffsetFromLive: Float?,
|
|
44
|
+
@Field var currentLiveTimestamp: Long?,
|
|
45
|
+
@Field var bufferedPosition: Double = .0
|
|
46
|
+
) : VideoEventPayload
|
|
47
|
+
|
|
48
|
+
class SubtitleTrackChangedEventPayload(
|
|
49
|
+
@Field val subtitleTrack: SubtitleTrack?,
|
|
50
|
+
@Field val oldSubtitleTrack: SubtitleTrack?
|
|
51
|
+
) : VideoEventPayload
|
|
52
|
+
|
|
53
|
+
class AudioTrackChangedEventPayload(
|
|
54
|
+
@Field val audioTrack: AudioTrack?,
|
|
55
|
+
@Field val oldAudioTrack: AudioTrack?
|
|
56
|
+
) : VideoEventPayload
|
|
57
|
+
|
|
58
|
+
class VideoTrackChangedEventPayload(
|
|
59
|
+
@Field val videoTrack: VideoTrack?,
|
|
60
|
+
@Field val oldVideoTrack: VideoTrack?
|
|
61
|
+
) : VideoEventPayload
|
|
62
|
+
|
|
63
|
+
class AvailableSubtitleTracksChangedEventPayload(
|
|
64
|
+
@Field val availableSubtitleTracks: List<SubtitleTrack>,
|
|
65
|
+
@Field val oldAvailableSubtitleTracks: List<SubtitleTrack>
|
|
66
|
+
) : VideoEventPayload
|
|
67
|
+
|
|
68
|
+
class AvailableAudioTracksChangedEventPayload(
|
|
69
|
+
@Field val availableAudioTracks: List<AudioTrack>,
|
|
70
|
+
@Field val oldAvailableAudioTracks: List<AudioTrack>
|
|
71
|
+
) : VideoEventPayload
|
|
72
|
+
|
|
73
|
+
class VideoSourceLoadedEventPayload(
|
|
74
|
+
@Field val videoSource: VideoSource?,
|
|
75
|
+
@Field val duration: Double,
|
|
76
|
+
@Field val availableVideoTracks: List<VideoTrack>,
|
|
77
|
+
@Field val availableSubtitleTracks: List<SubtitleTrack>,
|
|
78
|
+
@Field val availableAudioTracks: List<AudioTrack>
|
|
79
|
+
) : VideoEventPayload
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package expo.modules.video.records
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
import expo.modules.kotlin.records.Field
|
|
5
|
+
import expo.modules.kotlin.records.Record
|
|
6
|
+
import java.io.Serializable
|
|
7
|
+
|
|
8
|
+
class VideoMetadata(
|
|
9
|
+
@Field var title: String? = null,
|
|
10
|
+
@Field var artist: String? = null,
|
|
11
|
+
@Field var artwork: Uri? = null
|
|
12
|
+
) : Record, Serializable
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package expo.modules.video.records
|
|
2
|
+
|
|
3
|
+
import androidx.media3.common.Format
|
|
4
|
+
import expo.modules.kotlin.records.Field
|
|
5
|
+
import expo.modules.kotlin.records.Record
|
|
6
|
+
import java.io.Serializable
|
|
7
|
+
|
|
8
|
+
data class VideoSize(
|
|
9
|
+
@Field val width: Int = 0,
|
|
10
|
+
@Field val height: Int = 0
|
|
11
|
+
) : Record, Serializable {
|
|
12
|
+
constructor(size: androidx.media3.common.VideoSize) : this(size.width, size.height)
|
|
13
|
+
constructor(format: Format) : this(format.width, format.height)
|
|
14
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package expo.modules.video.records
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.ContentResolver
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import androidx.annotation.OptIn
|
|
9
|
+
import androidx.media3.common.MediaItem
|
|
10
|
+
import androidx.media3.common.MediaMetadata
|
|
11
|
+
import androidx.media3.common.util.UnstableApi
|
|
12
|
+
import androidx.media3.datasource.DataSpec
|
|
13
|
+
import androidx.media3.datasource.RawResourceDataSource
|
|
14
|
+
import androidx.media3.exoplayer.source.MediaSource
|
|
15
|
+
import expo.modules.kotlin.records.Field
|
|
16
|
+
import expo.modules.kotlin.records.Record
|
|
17
|
+
import expo.modules.video.UnsupportedDRMTypeException
|
|
18
|
+
import expo.modules.video.buildExpoVideoMediaSource
|
|
19
|
+
import expo.modules.video.enums.ContentType
|
|
20
|
+
import java.io.Serializable
|
|
21
|
+
|
|
22
|
+
@OptIn(UnstableApi::class)
|
|
23
|
+
class VideoSource(
|
|
24
|
+
@Field var uri: Uri? = null,
|
|
25
|
+
@Field var drm: DRMOptions? = null,
|
|
26
|
+
@Field var metadata: VideoMetadata? = null,
|
|
27
|
+
@Field var headers: Map<String, String>? = null,
|
|
28
|
+
@Field var useCaching: Boolean = false,
|
|
29
|
+
@Field val contentType: ContentType = ContentType.AUTO
|
|
30
|
+
) : Record, Serializable {
|
|
31
|
+
private fun toMediaId(): String {
|
|
32
|
+
return "uri:${this.uri}" +
|
|
33
|
+
"Headers: ${this.headers}" +
|
|
34
|
+
"DrmType:${this.drm?.type}" +
|
|
35
|
+
"DrmLicenseServer:${this.drm?.licenseServer}" +
|
|
36
|
+
"DrmMultiKey:${this.drm?.multiKey}" +
|
|
37
|
+
"DRMHeadersKeys:${this.drm?.headers?.keys?.joinToString { it }}}" +
|
|
38
|
+
"DRMHeadersValues:${this.drm?.headers?.values?.joinToString { it }}}" +
|
|
39
|
+
"NotificationDataTitle:${this.metadata?.title}" +
|
|
40
|
+
"NotificationDataSecondaryText:${this.metadata?.artist}" +
|
|
41
|
+
"NotificationDataArtwork:${this.metadata?.artwork?.path}" +
|
|
42
|
+
"ContentType:${this.contentType.value}"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fun toMediaSource(context: Context): MediaSource? {
|
|
46
|
+
this.uri ?: return null
|
|
47
|
+
return buildExpoVideoMediaSource(context, this)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fun toMediaItem(context: Context) = MediaItem
|
|
51
|
+
.Builder()
|
|
52
|
+
.apply {
|
|
53
|
+
setUri(parseLocalAssetId(uri, context))
|
|
54
|
+
contentType.toMimeTypeString()?.let {
|
|
55
|
+
setMimeType(it)
|
|
56
|
+
}
|
|
57
|
+
drm?.let {
|
|
58
|
+
if (it.type.isSupported()) {
|
|
59
|
+
setDrmConfiguration(it.toDRMConfiguration())
|
|
60
|
+
} else {
|
|
61
|
+
throw UnsupportedDRMTypeException(it.type)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
setMediaMetadata(
|
|
65
|
+
MediaMetadata.Builder().apply {
|
|
66
|
+
metadata?.let { data ->
|
|
67
|
+
setTitle(data.title)
|
|
68
|
+
setArtist(data.artist)
|
|
69
|
+
data.artwork?.let {
|
|
70
|
+
setArtworkUri(it)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}.build()
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
.build()
|
|
77
|
+
|
|
78
|
+
// Using `resolveAssetSource` to generate a local asset URI returns a resource name for android release builds
|
|
79
|
+
// we have to get the raw resource URI to play the video
|
|
80
|
+
@SuppressLint("DiscouragedApi") // AFAIK, in this case, there's no other way to get the resource URI
|
|
81
|
+
private fun parseLocalAssetId(uri: Uri?, context: Context): Uri? {
|
|
82
|
+
if (uri == null || uri.scheme != null) {
|
|
83
|
+
return uri
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
val resourceId: Int = context.resources.getIdentifier(
|
|
87
|
+
uri.toString(),
|
|
88
|
+
"raw",
|
|
89
|
+
context.packageName
|
|
90
|
+
)
|
|
91
|
+
val parsedUri = Uri.Builder()
|
|
92
|
+
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
|
|
93
|
+
.appendPath(resourceId.toString())
|
|
94
|
+
.build()
|
|
95
|
+
val dataSpec = DataSpec(parsedUri)
|
|
96
|
+
val rawResourceDataSource = RawResourceDataSource(context)
|
|
97
|
+
rawResourceDataSource.open(dataSpec)
|
|
98
|
+
return rawResourceDataSource.uri
|
|
99
|
+
} catch (e: RawResourceDataSource.RawResourceDataSourceException) {
|
|
100
|
+
Log.e("ExpoVideo", "Error parsing local asset id, falling back to original uri", e)
|
|
101
|
+
return uri
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package expo.modules.video.records
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.records.Field
|
|
4
|
+
import expo.modules.kotlin.records.Record
|
|
5
|
+
|
|
6
|
+
class VideoThumbnailOptions(
|
|
7
|
+
@Field val maxWidth: Int? = null,
|
|
8
|
+
@Field val maxHeight: Int? = null
|
|
9
|
+
) : Record {
|
|
10
|
+
// Returns a pair of Int values representing a valid size limitation for the thumbnail
|
|
11
|
+
// or null is no limit has been set
|
|
12
|
+
fun toNativeSizeLimit(): Pair<Int, Int>? {
|
|
13
|
+
if (this.maxWidth == null && this.maxHeight == null) {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
val width = this.maxWidth ?: Int.MAX_VALUE
|
|
17
|
+
val height = this.maxHeight ?: Int.MAX_VALUE
|
|
18
|
+
|
|
19
|
+
require(width >= 1 && height >= 1) {
|
|
20
|
+
"Failed to generate a thumbnail: The maxWidth and maxHeight parameters must be greater than zero"
|
|
21
|
+
}
|
|
22
|
+
return width to height
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.pm.ApplicationInfo
|
|
5
|
+
import androidx.annotation.OptIn
|
|
6
|
+
import androidx.media3.common.util.UnstableApi
|
|
7
|
+
import androidx.media3.common.util.Util
|
|
8
|
+
import androidx.media3.datasource.DataSource
|
|
9
|
+
import androidx.media3.datasource.DefaultDataSource
|
|
10
|
+
import androidx.media3.datasource.cache.CacheDataSource
|
|
11
|
+
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
|
12
|
+
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
|
13
|
+
import androidx.media3.exoplayer.source.MediaSource
|
|
14
|
+
import expo.modules.video.records.VideoSource
|
|
15
|
+
import okhttp3.OkHttpClient
|
|
16
|
+
|
|
17
|
+
@OptIn(UnstableApi::class)
|
|
18
|
+
fun buildBaseDataSourceFactory(context: Context, videoSource: VideoSource): DataSource.Factory {
|
|
19
|
+
return if (videoSource.uri?.scheme?.startsWith("http") == true) {
|
|
20
|
+
buildOkHttpDataSourceFactory(context, videoSource)
|
|
21
|
+
} else {
|
|
22
|
+
DefaultDataSource.Factory(context)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@OptIn(UnstableApi::class)
|
|
27
|
+
fun buildOkHttpDataSourceFactory(context: Context, videoSource: VideoSource): OkHttpDataSource.Factory {
|
|
28
|
+
val client = OkHttpClient.Builder().build()
|
|
29
|
+
|
|
30
|
+
// If the application name has ANY non-ASCII characters, we need to strip them out. This is because using non-ASCII characters
|
|
31
|
+
// in the User-Agent header can cause issues with getting the media to play.
|
|
32
|
+
val applicationName = getApplicationName(context).filter { it.code in 0..127 }
|
|
33
|
+
|
|
34
|
+
val defaultUserAgent = Util.getUserAgent(context, applicationName)
|
|
35
|
+
|
|
36
|
+
return OkHttpDataSource.Factory(client).apply {
|
|
37
|
+
val headers = videoSource.headers
|
|
38
|
+
headers?.takeIf { it.isNotEmpty() }?.let {
|
|
39
|
+
setDefaultRequestProperties(it)
|
|
40
|
+
}
|
|
41
|
+
val userAgent = headers?.get("User-Agent") ?: defaultUserAgent
|
|
42
|
+
setUserAgent(userAgent)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@OptIn(UnstableApi::class)
|
|
47
|
+
fun buildCacheDataSourceFactory(context: Context, videoSource: VideoSource): DataSource.Factory {
|
|
48
|
+
return CacheDataSource.Factory().apply {
|
|
49
|
+
setCache(VideoManager.cache.instance)
|
|
50
|
+
setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
|
|
51
|
+
setUpstreamDataSourceFactory(buildBaseDataSourceFactory(context, videoSource))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fun buildMediaSourceFactory(context: Context, dataSourceFactory: DataSource.Factory): MediaSource.Factory {
|
|
56
|
+
return DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@OptIn(UnstableApi::class)
|
|
60
|
+
fun buildExpoVideoMediaSource(context: Context, videoSource: VideoSource): MediaSource {
|
|
61
|
+
val dataSourceFactory = if (videoSource.useCaching) {
|
|
62
|
+
buildCacheDataSourceFactory(context, videoSource)
|
|
63
|
+
} else {
|
|
64
|
+
buildBaseDataSourceFactory(context, videoSource)
|
|
65
|
+
}
|
|
66
|
+
val mediaSourceFactory = buildMediaSourceFactory(context, dataSourceFactory)
|
|
67
|
+
val mediaItem = videoSource.toMediaItem(context)
|
|
68
|
+
return mediaSourceFactory.createMediaSource(mediaItem)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private fun getApplicationName(context: Context): String {
|
|
72
|
+
val applicationInfo: ApplicationInfo = context.applicationInfo
|
|
73
|
+
val stringId = applicationInfo.labelRes
|
|
74
|
+
return if (stringId == 0) applicationInfo.nonLocalizedLabel.toString() else context.getString(stringId)
|
|
75
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package expo.modules.video.utils
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import android.view.MotionEvent
|
|
5
|
+
import android.view.View
|
|
6
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
7
|
+
import com.facebook.react.uimanager.events.EventDispatcher
|
|
8
|
+
import com.facebook.react.uimanager.events.TouchEvent
|
|
9
|
+
import com.facebook.react.uimanager.events.TouchEventCoalescingKeyHelper
|
|
10
|
+
import com.facebook.react.uimanager.events.TouchEventType
|
|
11
|
+
|
|
12
|
+
internal fun EventDispatcher.dispatchMotionEvent(view: View, event: MotionEvent?, touchEventCoalescingKeyHelper: TouchEventCoalescingKeyHelper) {
|
|
13
|
+
if (event == null) {
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
val event = TouchEvent.obtain(
|
|
19
|
+
UIManagerHelper.getSurfaceId(view),
|
|
20
|
+
view.id,
|
|
21
|
+
event.toTouchEventType(),
|
|
22
|
+
event,
|
|
23
|
+
event.eventTime,
|
|
24
|
+
event.x,
|
|
25
|
+
event.y,
|
|
26
|
+
touchEventCoalescingKeyHelper
|
|
27
|
+
)
|
|
28
|
+
dispatchEvent(event)
|
|
29
|
+
} catch (e: RuntimeException) {
|
|
30
|
+
// We are not expecting any issues, but we want to prevent crashes in case the dispatch fails for any reason.
|
|
31
|
+
Log.e("EventDispatcherUtils", "Error dispatching touch event", e)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private fun MotionEvent.toTouchEventType(): TouchEventType {
|
|
36
|
+
return when (this.actionMasked) {
|
|
37
|
+
MotionEvent.ACTION_DOWN -> TouchEventType.START
|
|
38
|
+
MotionEvent.ACTION_UP -> TouchEventType.END
|
|
39
|
+
MotionEvent.ACTION_MOVE -> TouchEventType.MOVE
|
|
40
|
+
MotionEvent.ACTION_CANCEL -> TouchEventType.CANCEL
|
|
41
|
+
else -> TouchEventType.CANCEL
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package expo.modules.video.utils
|
|
2
|
+
|
|
3
|
+
import java.lang.ref.WeakReference
|
|
4
|
+
|
|
5
|
+
internal class MutableWeakReference<T>(value: T? = null) {
|
|
6
|
+
private var ref: WeakReference<T> = WeakReference(value)
|
|
7
|
+
|
|
8
|
+
fun get(): T? {
|
|
9
|
+
return ref.get()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
fun set(value: T) {
|
|
13
|
+
ref = WeakReference(value)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
package expo.modules.video.utils
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.app.PictureInPictureParams
|
|
5
|
+
import android.graphics.Rect
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import android.util.Rational
|
|
9
|
+
import androidx.annotation.OptIn
|
|
10
|
+
import androidx.media3.common.VideoSize
|
|
11
|
+
import androidx.media3.common.util.UnstableApi
|
|
12
|
+
import androidx.media3.ui.PlayerView
|
|
13
|
+
import expo.modules.video.PictureInPictureConfigurationException
|
|
14
|
+
import expo.modules.video.VideoView.Companion.isPictureInPictureSupported
|
|
15
|
+
import expo.modules.video.enums.ContentFit
|
|
16
|
+
|
|
17
|
+
@OptIn(UnstableApi::class)
|
|
18
|
+
internal fun calculateRectHint(playerView: PlayerView): Rect {
|
|
19
|
+
val hint = Rect()
|
|
20
|
+
playerView.videoSurfaceView?.getGlobalVisibleRect(hint)
|
|
21
|
+
val location = IntArray(2)
|
|
22
|
+
playerView.videoSurfaceView?.getLocationOnScreen(location)
|
|
23
|
+
|
|
24
|
+
// getGlobalVisibleRect doesn't take into account the offset for the notch, we use the screen location
|
|
25
|
+
// of the view to calculate the rectHint.
|
|
26
|
+
// We only apply this correction on the y axis due to something that looks like a bug in the Android SDK.
|
|
27
|
+
// If the video screen and home screen have the same orientation this works correctly,
|
|
28
|
+
// but if the home screen doesn't support landscape and the video screen does, we have to
|
|
29
|
+
// ignore the offset for the notch on the x axis even though it's present on the video screen
|
|
30
|
+
// because there will be no offset on the home screen
|
|
31
|
+
// there is no way to check the orientation support of the home screen, so we make the bet that
|
|
32
|
+
// it won't support landscape (as most android home screens do by default)
|
|
33
|
+
// This doesn't have any serious consequences if we are wrong with the guess, the transition will be a bit off though
|
|
34
|
+
val height = hint.bottom - hint.top
|
|
35
|
+
hint.top = location[1]
|
|
36
|
+
hint.bottom = hint.top + height
|
|
37
|
+
return hint
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
internal fun calculatePiPAspectRatio(videoSize: VideoSize, viewWidth: Int, viewHeight: Int, contentFit: ContentFit): Rational {
|
|
41
|
+
var aspectRatio = if (contentFit == ContentFit.CONTAIN) {
|
|
42
|
+
Rational(videoSize.width, videoSize.height)
|
|
43
|
+
} else {
|
|
44
|
+
Rational(viewWidth, viewHeight)
|
|
45
|
+
}
|
|
46
|
+
// AspectRatio for the activity in picture-in-picture, must be between 2.39:1 and 1:2.39 (inclusive).
|
|
47
|
+
// https://developer.android.com/reference/android/app/PictureInPictureParams.Builder#setAspectRatio(android.util.Rational)
|
|
48
|
+
val maximumRatio = Rational(239, 100)
|
|
49
|
+
val minimumRatio = Rational(100, 239)
|
|
50
|
+
|
|
51
|
+
if (aspectRatio.toFloat() > maximumRatio.toFloat()) {
|
|
52
|
+
aspectRatio = maximumRatio
|
|
53
|
+
} else if (aspectRatio.toFloat() < minimumRatio.toFloat()) {
|
|
54
|
+
aspectRatio = minimumRatio
|
|
55
|
+
}
|
|
56
|
+
return aspectRatio
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
internal fun applyRectHint(activity: Activity, rectHint: Rect) {
|
|
60
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPictureSupported(activity)) {
|
|
61
|
+
runWithPiPMisconfigurationSoftHandling {
|
|
62
|
+
activity.setPictureInPictureParams(PictureInPictureParams.Builder().setSourceRectHint(rectHint).build())
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// We can't check if AndroidManifest.xml is configured properly, so we have to handle the exceptions ourselves to prevent crashes
|
|
68
|
+
internal fun runWithPiPMisconfigurationSoftHandling(shouldThrow: Boolean = false, block: () -> Any?) {
|
|
69
|
+
try {
|
|
70
|
+
block()
|
|
71
|
+
} catch (e: IllegalStateException) {
|
|
72
|
+
Log.e("ExpoVideo", "Current activity does not support picture-in-picture. Make sure you have configured the `expo-video` config plugin correctly.")
|
|
73
|
+
if (shouldThrow) {
|
|
74
|
+
throw PictureInPictureConfigurationException()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
internal fun applyPiPParams(activity: Activity, autoEnterPiP: Boolean, aspectRatio: Rational? = null) {
|
|
80
|
+
// If the aspect ratio exceeds the limits, the app will crash
|
|
81
|
+
val safeAspectRatio = aspectRatio?.takeIf { it.toFloat() in 0.41841..2.39 }
|
|
82
|
+
|
|
83
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPictureSupported(activity)) {
|
|
84
|
+
val paramsBuilder = PictureInPictureParams.Builder()
|
|
85
|
+
|
|
86
|
+
safeAspectRatio?.let {
|
|
87
|
+
paramsBuilder.setAspectRatio(it)
|
|
88
|
+
}
|
|
89
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
90
|
+
paramsBuilder.setAutoEnterEnabled(autoEnterPiP)
|
|
91
|
+
}
|
|
92
|
+
runWithPiPMisconfigurationSoftHandling {
|
|
93
|
+
activity.setPictureInPictureParams(paramsBuilder.build())
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package expo.modules.video.utils
|
|
2
|
+
|
|
3
|
+
import com.facebook.yoga.YogaConstants
|
|
4
|
+
|
|
5
|
+
fun Float.ifYogaUndefinedUse(value: Float) =
|
|
6
|
+
if (YogaConstants.isUndefined(this)) {
|
|
7
|
+
value
|
|
8
|
+
} else {
|
|
9
|
+
this
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
inline fun Float.ifYogaDefinedUse(transformFun: (current: Float) -> Float) =
|
|
13
|
+
if (YogaConstants.isUndefined(this)) {
|
|
14
|
+
this
|
|
15
|
+
} else {
|
|
16
|
+
transformFun(this)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fun makeYogaUndefinedIfNegative(value: Float) =
|
|
20
|
+
if (!YogaConstants.isUndefined(value) && value < 0) YogaConstants.UNDEFINED else value
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
android:width="24dp"
|
|
4
|
+
android:height="24dp"
|
|
5
|
+
android:viewportWidth="24"
|
|
6
|
+
android:viewportHeight="24">
|
|
7
|
+
<group
|
|
8
|
+
android:pivotX="12"
|
|
9
|
+
android:pivotY="4"
|
|
10
|
+
android:scaleX="1.15"
|
|
11
|
+
android:scaleY="1.15">
|
|
12
|
+
<path
|
|
13
|
+
android:fillColor="?android:attr/colorControlNormal"
|
|
14
|
+
android:pathData="M12,5V1L7,6l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6H4c0,4.42 3.58,8 8,8s8,-3.58 8,-8 -3.58,-8 -8,-8z" />
|
|
15
|
+
<group
|
|
16
|
+
android:pivotX="12"
|
|
17
|
+
android:pivotY="8"
|
|
18
|
+
android:scaleX="1"
|
|
19
|
+
android:scaleY="0.9">
|
|
20
|
+
<path
|
|
21
|
+
android:fillColor="?android:attr/colorControlNormal"
|
|
22
|
+
android:pathData="m10.412 17.244h-0.70313v-4.4805q-0.25391 0.24219-0.66797 0.48438-0.41016 0.24219-0.73828 0.36328v-0.67969q0.58984-0.27734 1.0313-0.67188 0.44141-0.39453 0.625-0.76563h0.45313zm1.2008-2.8242q0-1.0156 0.20703-1.6328 0.21094-0.6211 0.6211-0.95703 0.41406-0.33594 1.0391-0.33594 0.46094 0 0.80859 0.1875 0.34766 0.18359 0.57422 0.53516 0.22656 0.34766 0.35547 0.85156 0.12891 0.5 0.12891 1.3516 0 1.0078-0.20703 1.6289-0.20703 0.61719-0.6211 0.95703-0.41016 0.33594-1.0391 0.33594-0.82813 0-1.3008-0.59375-0.56641-0.71484-0.56641-2.3281zm0.72266 0q0 1.4102 0.32812 1.8789 0.33203 0.46484 0.81641 0.46484 0.48438 0 0.8125-0.46875 0.33203-0.46875 0.33203-1.875 0-1.4141-0.33203-1.8789-0.32813-0.46484-0.82031-0.46484-0.48438 0-0.77344 0.41016-0.36328 0.52344-0.36328 1.9336z"/>
|
|
23
|
+
</group>
|
|
24
|
+
</group>
|
|
25
|
+
</vector>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
android:width="24dp"
|
|
4
|
+
android:height="24dp"
|
|
5
|
+
android:viewportWidth="24"
|
|
6
|
+
android:viewportHeight="24">
|
|
7
|
+
<group
|
|
8
|
+
android:pivotX="12"
|
|
9
|
+
android:pivotY="4"
|
|
10
|
+
android:scaleX="1.15"
|
|
11
|
+
android:scaleY="1.15">
|
|
12
|
+
<path
|
|
13
|
+
android:fillColor="?android:attr/colorControlNormal"
|
|
14
|
+
android:pathData="M12,5V1L7,6l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6H4c0,4.42 3.58,8 8,8s8,-3.58 8,-8 -3.58,-8 -8,-8z" />
|
|
15
|
+
<group
|
|
16
|
+
android:pivotX="12"
|
|
17
|
+
android:pivotY="8"
|
|
18
|
+
android:scaleX="0.9"
|
|
19
|
+
android:scaleY="0.9">
|
|
20
|
+
<path
|
|
21
|
+
android:fillColor="?android:attr/colorControlNormal"
|
|
22
|
+
android:pathData="m10.412 17.244h-0.70313v-4.4805q-0.25391 0.24219-0.66797 0.48438-0.41016 0.24219-0.73828 0.36328v-0.67969q0.58984-0.27734 1.0313-0.67188 0.44141-0.39453 0.625-0.76563h0.45313zm1.2008-1.5 0.73828-0.0625q0.08203 0.53906 0.37891 0.8125 0.30078 0.26953 0.72266 0.26953 0.50781 0 0.85938-0.38281t0.35156-1.0156q0-0.60156-0.33984-0.94922-0.33594-0.34766-0.88281-0.34766-0.33984 0-0.61328 0.15625-0.27344 0.15234-0.42969 0.39844l-0.66016-0.08594 0.55469-2.9414h2.8477v0.67188h-2.2852l-0.30859 1.5391q0.51562-0.35938 1.082-0.35938 0.75 0 1.2656 0.51953 0.51562 0.51953 0.51562 1.3359 0 0.77734-0.45312 1.3438-0.55078 0.69531-1.5039 0.69531-0.78125 0-1.2773-0.4375-0.49219-0.4375-0.5625-1.1602z" />
|
|
23
|
+
</group>
|
|
24
|
+
</group>
|
|
25
|
+
</vector>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
android:width="24dp"
|
|
4
|
+
android:height="24dp"
|
|
5
|
+
android:viewportWidth="24"
|
|
6
|
+
android:viewportHeight="24">
|
|
7
|
+
<group
|
|
8
|
+
android:pivotX="12"
|
|
9
|
+
android:pivotY="4"
|
|
10
|
+
android:scaleX="1.15"
|
|
11
|
+
android:scaleY="1.15">
|
|
12
|
+
<path
|
|
13
|
+
android:fillColor="?android:attr/colorControlNormal"
|
|
14
|
+
android:pathData="M12,5V1L7,6l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6H4c0,4.42 3.58,8 8,8s8,-3.58 8,-8 -3.58,-8 -8,-8z" />
|
|
15
|
+
<group
|
|
16
|
+
android:pivotX="12"
|
|
17
|
+
android:pivotY="8"
|
|
18
|
+
android:scaleX="0.9"
|
|
19
|
+
android:scaleY="0.9">
|
|
20
|
+
<path
|
|
21
|
+
android:fillColor="?android:attr/colorControlNormal"
|
|
22
|
+
android:pathData="m 10.030088,16.051511 0.770367,-0.06521 q 0.08559,0.562491 0.395373,0.847811 0.313853,0.281245 0.754062,0.281245 0.529882,0 0.896723,-0.399449 0.366841,-0.39945 0.366841,-1.059764 0,-0.627706 -0.354612,-0.990471 -0.350538,-0.362765 -0.92118,-0.362765 -0.354613,0 -0.639933,0.163041 -0.285322,0.158964 -0.448362,0.415752 l -0.688846,-0.08967 0.578794,-3.069238 h 2.971413 v 0.701074 H 11.32626 l -0.322005,1.60595 q 0.538034,-0.374993 1.129056,-0.374993 0.782594,0 1.320628,0.54211 0.538034,0.542109 0.538034,1.393996 0,0.811127 -0.472817,1.402149 -0.574718,0.72553 -1.569266,0.72553 -0.815202,0 -1.332856,-0.456514 -0.513578,-0.456516 -0.586946,-1.210578 z" />
|
|
23
|
+
</group>
|
|
24
|
+
</group>
|
|
25
|
+
</vector>
|