mirror of
https://github.com/zoriya/react-native-video.git
synced 2026-05-27 16:42:41 +00:00
feat: add getAssetInformationAsync function to source object
This commit is contained in:
+2
-1
@@ -18,7 +18,8 @@ Pod::Spec.new do |s|
|
||||
|
||||
s.source_files = [
|
||||
"ios/*.{h,m,mm,swift}",
|
||||
"ios/hybrids/*.{h,m,mm,swift}", # Nitro Hybrid files
|
||||
"ios/core/**/*.{h,m,mm,swift}", # Core library files
|
||||
"ios/hybrids/**/*.{h,m,mm,swift}", # Nitro Hybrid files
|
||||
"ios/view/**/*.{h,m,mm,swift}" # Video View files
|
||||
]
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ package com.margelo.nitro.video
|
||||
|
||||
import androidx.media3.common.MediaItem
|
||||
import com.facebook.proguard.annotations.DoNotStrip
|
||||
import com.margelo.nitro.core.Promise
|
||||
import com.video.utils.AssetUtils
|
||||
|
||||
@DoNotStrip
|
||||
class HybridVideoPlayerSource(): HybridVideoPlayerSourceSpec() {
|
||||
override lateinit var uri: String
|
||||
|
||||
lateinit var mediaItem: MediaItem
|
||||
|
||||
constructor(uri: String) : this() {
|
||||
@@ -13,6 +16,12 @@ class HybridVideoPlayerSource(): HybridVideoPlayerSourceSpec() {
|
||||
this.mediaItem = MediaItem.fromUri(uri)
|
||||
}
|
||||
|
||||
override fun getAssetInformationAsync(): Promise<VideoInformation> {
|
||||
return Promise.async {
|
||||
return@async AssetUtils.getAssetInformation(uri)
|
||||
}
|
||||
}
|
||||
|
||||
override val memorySize: Long
|
||||
get() = 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.video.utils
|
||||
|
||||
import android.media.MediaFormat
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.webkit.URLUtil
|
||||
import com.margelo.nitro.video.VideoInformation
|
||||
import com.margelo.nitro.video.VideoOrientation
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.net.URLConnection
|
||||
|
||||
class AssetUtils {
|
||||
companion object {
|
||||
fun getAssetInformation(uri: String): VideoInformation {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
|
||||
when {
|
||||
URLUtil.isFileUrl(uri) -> {
|
||||
retriever.setDataSource(Uri.parse(uri).path)
|
||||
}
|
||||
else -> {
|
||||
//TODO: pass headers here
|
||||
retriever.setDataSource(uri, HashMap<String, String>())
|
||||
}
|
||||
}
|
||||
|
||||
// Get dimensions
|
||||
val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toDoubleOrNull() ?: Double.NaN
|
||||
val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toDoubleOrNull() ?: Double.NaN
|
||||
|
||||
// Get duration in milliseconds, convert to long
|
||||
val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() ?: -1L
|
||||
|
||||
// If we have some valid info, but there is no duration it might be live
|
||||
val isLive = !width.isNaN() && !height.isNaN() && duration <= 0
|
||||
|
||||
// Get bitrate
|
||||
val bitrate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)?.toDoubleOrNull() ?: Double.NaN
|
||||
|
||||
// Get rotation
|
||||
val rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull() ?: 0
|
||||
|
||||
// Check for HDR by looking at color transfer (API 30+)
|
||||
val isHDR = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val colorTransfer = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER)?.toIntOrNull()
|
||||
colorTransfer == MediaFormat.COLOR_TRANSFER_ST2084 || colorTransfer == MediaFormat.COLOR_TRANSFER_HLG
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
// Clean up
|
||||
retriever.release()
|
||||
|
||||
// Get file size
|
||||
val fileSize = getFileSizeFromUri(uri)
|
||||
|
||||
val videoInfo = VideoInformation(
|
||||
bitrate = bitrate,
|
||||
width = width,
|
||||
height = height,
|
||||
duration = duration,
|
||||
fileSize = fileSize,
|
||||
isHDR = isHDR,
|
||||
isLive = isLive,
|
||||
orientation = getVideoOrientation(width.toInt(), height.toInt(), rotation)
|
||||
)
|
||||
|
||||
return videoInfo
|
||||
}
|
||||
|
||||
fun getFileSizeFromUri(uri: String): Long {
|
||||
return try {
|
||||
when {
|
||||
URLUtil.isFileUrl(uri) -> {
|
||||
val file = File(Uri.parse(uri).path ?: return -1)
|
||||
if (file.exists()) file.length() else -1
|
||||
}
|
||||
URLUtil.isNetworkUrl(uri) -> {
|
||||
val connection: URLConnection = URL(uri).openConnection()
|
||||
connection.connect()
|
||||
connection.contentLength.toLong()
|
||||
}
|
||||
else -> -1
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
fun getVideoOrientation(width: Int?, height: Int?, rotation: Int?): VideoOrientation {
|
||||
if (width == 0 || height == 0 || height == null || width == null) return VideoOrientation.UNKNOWN
|
||||
|
||||
// Check if video is portrait or landscape using natural size
|
||||
val isNaturalSizePortrait = height > width
|
||||
|
||||
// If rotation is not available, use natural size to determine orientation
|
||||
if (rotation == null) {
|
||||
return if (isNaturalSizePortrait) VideoOrientation.PORTRAIT else VideoOrientation.LANDSCAPE_RIGHT
|
||||
}
|
||||
|
||||
// Normalize rotation to 0-360 range
|
||||
val normalizedRotation = ((rotation % 360) + 360) % 360
|
||||
|
||||
return when (normalizedRotation) {
|
||||
0 -> if (isNaturalSizePortrait) VideoOrientation.PORTRAIT else VideoOrientation.LANDSCAPE_RIGHT
|
||||
90 -> VideoOrientation.PORTRAIT
|
||||
180 -> if (isNaturalSizePortrait) VideoOrientation.PORTRAIT_UPSIDE_DOWN else VideoOrientation.LANDSCAPE_LEFT
|
||||
270 -> VideoOrientation.PORTRAIT_UPSIDE_DOWN
|
||||
else -> if (isNaturalSizePortrait) VideoOrientation.PORTRAIT else VideoOrientation.LANDSCAPE_RIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// AVAssetUtils.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by Krzysztof Moch on 16/01/2025.
|
||||
//
|
||||
import AVFoundation
|
||||
|
||||
public final class AVAssetUtils {
|
||||
public static func getAssetInformation(for asset: AVAsset) async throws -> VideoInformation {
|
||||
// Initialize with default values
|
||||
var videoInformation = VideoInformation(
|
||||
bitrate: Double.nan,
|
||||
width: Double.nan,
|
||||
height: Double.nan,
|
||||
duration: -1,
|
||||
fileSize: -1,
|
||||
isHDR: false,
|
||||
isLive: false,
|
||||
orientation: .unknown
|
||||
)
|
||||
|
||||
videoInformation.fileSize = try await getFileSize(for: <#T##URL#>)
|
||||
|
||||
// Check if asset is live stream
|
||||
if asset.duration.flags.contains(.indefinite) {
|
||||
videoInformation.duration = -1
|
||||
videoInformation.isLive = true
|
||||
} else {
|
||||
videoInformation.duration = Int64(CMTimeGetSeconds(asset.duration))
|
||||
videoInformation.isLive = false
|
||||
}
|
||||
|
||||
if let videoTrack = asset.tracks(withMediaType: .video).first {
|
||||
let size = videoTrack.naturalSize.applying(videoTrack.preferredTransform)
|
||||
videoInformation.width = size.width
|
||||
videoInformation.height = size.height
|
||||
|
||||
videoInformation.bitrate = Double(videoTrack.estimatedDataRate)
|
||||
|
||||
videoInformation.orientation = getVideoOrientation(videoTrack: videoTrack)
|
||||
|
||||
if #available(iOS 14.0, tvOS 14.0, visionOS 1.0, *) {
|
||||
videoInformation.isHDR = videoTrack.hasMediaCharacteristic(.containsHDRVideo)
|
||||
}
|
||||
}
|
||||
|
||||
return videoInformation
|
||||
}
|
||||
|
||||
public static func getFileSize(for url: URL) async throws -> Int64 {
|
||||
if url.isFileURL {
|
||||
return try Int64(url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? -1)
|
||||
}
|
||||
|
||||
// Try to get file size from remote server
|
||||
// Make HEAD request to get content length
|
||||
// If content length is not available, returns -1
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "HEAD"
|
||||
|
||||
let (_, response) = try await URLSession.shared.data(for: request)
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
return -1
|
||||
}
|
||||
|
||||
let contentLength = httpResponse.allHeaderFields["Content-Length"] as? String
|
||||
return Int64(contentLength ?? "-1") ?? -1
|
||||
}
|
||||
|
||||
public static func getVideoOrientation(videoTrack: AVAssetTrack) -> VideoOrientation {
|
||||
let transform = videoTrack.preferredTransform
|
||||
let size = videoTrack.naturalSize.applying(transform)
|
||||
|
||||
// Check if video is portrait or landscape
|
||||
let isNaturalSizePortrait = size.width < size.height
|
||||
|
||||
// Calculate video rotation
|
||||
let angle = atan2(Double(transform.b), Double(transform.a))
|
||||
let degrees = angle * 180 / .pi
|
||||
let rotation = degrees < 0 ? degrees + 360 : degrees
|
||||
|
||||
switch rotation {
|
||||
case 0:
|
||||
return isNaturalSizePortrait ? .portrait : .landscapeRight
|
||||
case 90, -270:
|
||||
return .portrait
|
||||
case 180, -180:
|
||||
return isNaturalSizePortrait ? .portraitUpsideDown : .landscapeLeft
|
||||
case 270, -90:
|
||||
return .portraitUpsideDown
|
||||
default:
|
||||
return isNaturalSizePortrait ? .portrait : .landscape
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// VideoQueues.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by Krzysztof Moch on 16/01/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class VideoQueues {
|
||||
public static let videoAssetQueue = DispatchQueue(label: "RNVideo/videoAssetQueue",
|
||||
qos: .utility,
|
||||
attributes: [],
|
||||
autoreleaseFrequency: .inherit)
|
||||
}
|
||||
@@ -7,27 +7,57 @@
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import NitroModules
|
||||
|
||||
class HybridVideoPlayerSource: HybridVideoPlayerSourceSpec {
|
||||
var uri: String {
|
||||
didSet {
|
||||
guard let url = URL(string: uri) else {
|
||||
return
|
||||
public var asset: AVURLAsset?
|
||||
var uri: String
|
||||
|
||||
let url: URL
|
||||
|
||||
init(uri: String) throws {
|
||||
self.uri = uri
|
||||
|
||||
guard let url = URL(string: uri) else {
|
||||
throw RuntimeError.error(withMessage: "Invalid URL: \(uri)")
|
||||
}
|
||||
|
||||
self.url = url
|
||||
}
|
||||
|
||||
func getAssetInformationAsync() throws -> Promise<VideoInformation> {
|
||||
return Promise.async(.utility) { [weak self] in
|
||||
guard let self else {
|
||||
throw RuntimeError.error(withMessage: "HybridVideoPlayerSource has been deallocated")
|
||||
}
|
||||
playerItem = AVPlayerItem(url: url)
|
||||
|
||||
if self.url.isFileURL {
|
||||
try checkReadFilePermission(for: self.url)
|
||||
}
|
||||
|
||||
try initializeAsset()
|
||||
|
||||
guard let asset = self.asset else {
|
||||
throw RuntimeError.error(withMessage: "Failed to initialize asset")
|
||||
}
|
||||
|
||||
return try await AVAssetUtils.getAssetInformation(for: asset)
|
||||
}
|
||||
}
|
||||
var playerItem: AVPlayerItem?
|
||||
|
||||
init(uri: String) {
|
||||
self.uri = uri
|
||||
public func initializeAsset() throws {
|
||||
guard asset == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Pass headers here
|
||||
asset = AVURLAsset(url: url)
|
||||
}
|
||||
|
||||
// Initialize HybridContext
|
||||
var hybridContext = margelo.nitro.HybridContext()
|
||||
|
||||
// Return size of the instance to inform JS GC about memory pressure
|
||||
var memorySize: Int {
|
||||
return getSizeOf(self)
|
||||
private func checkReadFilePermission(for path: URL) throws {
|
||||
let fileManager = FileManager.default
|
||||
if !fileManager.isReadableFile(atPath: path.path) {
|
||||
throw RuntimeError.error(withMessage: "Cannot read file at path: \(path.path), is path \(path.path) correct? Does app have permission to read file?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
import type { HybridObject } from 'react-native-nitro-modules';
|
||||
import type { VideoInformation } from '../../types/VideoInformation';
|
||||
|
||||
/**
|
||||
* A source for a {@link VideoPlayer}.
|
||||
* Source cannot be changed after it is created. If you need to update the source, you need to create a new one.
|
||||
* It provides functions to get information about the asset.
|
||||
*/
|
||||
export interface VideoPlayerSource
|
||||
extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> {
|
||||
uri: string;
|
||||
/**
|
||||
* The URI of the asset.
|
||||
*/
|
||||
readonly uri: string;
|
||||
|
||||
/**
|
||||
* Get the information about the asset.
|
||||
*/
|
||||
getAssetInformationAsync(): Promise<VideoInformation>;
|
||||
}
|
||||
|
||||
export interface VideoPlayerSourceFactory
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
export type VideoOrientation = "portrait" | "landscape" | "portrait-upside-down" | "landscape-left" | "landscape-right" | "unknown";
|
||||
|
||||
export interface VideoInformation {
|
||||
/**
|
||||
* The bitrate of the video in kbps.
|
||||
*/
|
||||
bitrate: number;
|
||||
|
||||
/**
|
||||
* The width of the video in pixels.
|
||||
*/
|
||||
width: number;
|
||||
|
||||
/**
|
||||
* The height of the video in pixels.
|
||||
*/
|
||||
height: number;
|
||||
|
||||
/**
|
||||
* The duration of the video in seconds.
|
||||
*/
|
||||
duration: bigint;
|
||||
|
||||
/**
|
||||
* The file size of the video in bytes.
|
||||
*/
|
||||
fileSize: bigint;
|
||||
|
||||
/**
|
||||
* Whether the video is HDR.
|
||||
*/
|
||||
isHDR: boolean;
|
||||
|
||||
/**
|
||||
* Whether the video is live
|
||||
*/
|
||||
isLive: boolean;
|
||||
|
||||
/**
|
||||
* The orientation of the video.
|
||||
* see {@link VideoOrientation}
|
||||
*/
|
||||
orientation: VideoOrientation;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { VideoPlayer } from "../spec/nitro/VideoPlayer.nitro";
|
||||
import { useReleasingHybridObject } from "./useReleasingHybridObject";
|
||||
import { createPlayer } from "./factory";
|
||||
import type { VideoSource } from "../types/VideoConfig";
|
||||
import type { VideoConfig, VideoSource } from "../types/VideoConfig";
|
||||
|
||||
/**
|
||||
* A hook that creates and manages a video player.
|
||||
@@ -11,8 +11,8 @@ import type { VideoSource } from "../types/VideoConfig";
|
||||
* @returns VideoPlayer (see {@link VideoPlayer})
|
||||
*/
|
||||
export const useVideoPlayer = (
|
||||
source: VideoSource,
|
||||
setup: (player: VideoPlayer) => void | undefined
|
||||
source: VideoConfig | VideoSource,
|
||||
setup?: (player: VideoPlayer) => void
|
||||
) => {
|
||||
return useReleasingHybridObject(
|
||||
() => {
|
||||
|
||||
Reference in New Issue
Block a user