capacitor-video-transcoder 0.0.0-stage → 0.0.1

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.
Files changed (32) hide show
  1. package/CapacitorVideoTranscoder.podspec +18 -0
  2. package/Package.swift +39 -0
  3. package/README.md +238 -2
  4. package/android/build.gradle +68 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/kotlin/io/github/maximbelov/videotranscoder/TranscodeOptions.kt +122 -0
  7. package/android/src/main/kotlin/io/github/maximbelov/videotranscoder/VideoTranscoder.kt +265 -0
  8. package/android/src/main/kotlin/io/github/maximbelov/videotranscoder/VideoTranscoderPlugin.kt +51 -0
  9. package/android/src/main/res/.gitkeep +0 -0
  10. package/dist/docs.json +221 -0
  11. package/dist/esm/definitions.d.ts +120 -0
  12. package/dist/esm/definitions.js +2 -0
  13. package/dist/esm/definitions.js.map +1 -0
  14. package/dist/esm/index.d.ts +4 -0
  15. package/dist/esm/index.js +7 -0
  16. package/dist/esm/index.js.map +1 -0
  17. package/dist/esm/web.d.ts +6 -0
  18. package/dist/esm/web.js +10 -0
  19. package/dist/esm/web.js.map +1 -0
  20. package/dist/plugin.cjs.js +24 -0
  21. package/dist/plugin.cjs.js.map +1 -0
  22. package/dist/plugin.js +27 -0
  23. package/dist/plugin.js.map +1 -0
  24. package/ios/Package.swift +22 -0
  25. package/ios/Sources/VideoTranscoderCore/Geometry.swift +61 -0
  26. package/ios/Sources/VideoTranscoderCore/TranscodeOptions.swift +107 -0
  27. package/ios/Sources/VideoTranscoderCore/Transcoder.swift +388 -0
  28. package/ios/Sources/VideoTranscoderPlugin/VideoTranscoderPlugin.swift +73 -0
  29. package/ios/Tests/VideoTranscoderCoreTests/OptionsAndGeometryTests.swift +146 -0
  30. package/ios/Tests/VideoTranscoderCoreTests/TranscoderTests.swift +234 -0
  31. package/ios/Tests/VideoTranscoderPluginTests/VideoTranscoderPluginTests.swift +32 -0
  32. package/package.json +101 -4
@@ -0,0 +1,7 @@
1
+ import { registerPlugin } from '@capacitor/core';
2
+ const VideoTranscoder = registerPlugin('VideoTranscoder', {
3
+ web: () => import('./web').then((m) => new m.VideoTranscoderWeb()),
4
+ });
5
+ export * from './definitions';
6
+ export { VideoTranscoder };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,eAAe,GAAG,cAAc,CAAwB,iBAAiB,EAAE;IAC/E,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,kBAAkB,EAAE,CAAC;CACnE,CAAC,CAAC;AAEH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,CAAC","sourcesContent":["import { registerPlugin } from '@capacitor/core';\n\nimport type { VideoTranscoderPlugin } from './definitions';\n\nconst VideoTranscoder = registerPlugin<VideoTranscoderPlugin>('VideoTranscoder', {\n web: () => import('./web').then((m) => new m.VideoTranscoderWeb()),\n});\n\nexport * from './definitions';\nexport { VideoTranscoder };\n"]}
@@ -0,0 +1,6 @@
1
+ import { WebPlugin } from '@capacitor/core';
2
+ import type { TranscodeOptions, TranscodeResult, VideoTranscoderPlugin } from './definitions';
3
+ export declare class VideoTranscoderWeb extends WebPlugin implements VideoTranscoderPlugin {
4
+ transcode(options: TranscodeOptions): Promise<TranscodeResult>;
5
+ cancel(): Promise<void>;
6
+ }
@@ -0,0 +1,10 @@
1
+ import { WebPlugin } from '@capacitor/core';
2
+ export class VideoTranscoderWeb extends WebPlugin {
3
+ async transcode(options) {
4
+ throw this.unimplemented(`Cannot transcode ${options.source} on the web: it needs the platform's video encoder.`);
5
+ }
6
+ async cancel() {
7
+ throw this.unimplemented('cancel is not available on the web.');
8
+ }
9
+ }
10
+ //# sourceMappingURL=web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C,MAAM,OAAO,kBAAmB,SAAQ,SAAS;IAC/C,KAAK,CAAC,SAAS,CAAC,OAAyB;QACvC,MAAM,IAAI,CAAC,aAAa,CAAC,oBAAoB,OAAO,CAAC,MAAM,qDAAqD,CAAC,CAAC;IACpH,CAAC;IAED,KAAK,CAAC,MAAM;QACV,MAAM,IAAI,CAAC,aAAa,CAAC,qCAAqC,CAAC,CAAC;IAClE,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type { TranscodeOptions, TranscodeResult, VideoTranscoderPlugin } from './definitions';\n\nexport class VideoTranscoderWeb extends WebPlugin implements VideoTranscoderPlugin {\n async transcode(options: TranscodeOptions): Promise<TranscodeResult> {\n throw this.unimplemented(`Cannot transcode ${options.source} on the web: it needs the platform's video encoder.`);\n }\n\n async cancel(): Promise<void> {\n throw this.unimplemented('cancel is not available on the web.');\n }\n}\n"]}
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ var core = require('@capacitor/core');
4
+
5
+ const VideoTranscoder = core.registerPlugin('VideoTranscoder', {
6
+ web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.VideoTranscoderWeb()),
7
+ });
8
+
9
+ class VideoTranscoderWeb extends core.WebPlugin {
10
+ async transcode(options) {
11
+ throw this.unimplemented(`Cannot transcode ${options.source} on the web: it needs the platform's video encoder.`);
12
+ }
13
+ async cancel() {
14
+ throw this.unimplemented('cancel is not available on the web.');
15
+ }
16
+ }
17
+
18
+ var web = /*#__PURE__*/Object.freeze({
19
+ __proto__: null,
20
+ VideoTranscoderWeb: VideoTranscoderWeb
21
+ });
22
+
23
+ exports.VideoTranscoder = VideoTranscoder;
24
+ //# sourceMappingURL=plugin.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst VideoTranscoder = registerPlugin('VideoTranscoder', {\n web: () => import('./web').then((m) => new m.VideoTranscoderWeb()),\n});\nexport * from './definitions';\nexport { VideoTranscoder };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class VideoTranscoderWeb extends WebPlugin {\n async transcode(options) {\n throw this.unimplemented(`Cannot transcode ${options.source} on the web: it needs the platform's video encoder.`);\n }\n async cancel() {\n throw this.unimplemented('cancel is not available on the web.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,eAAe,GAAGA,mBAAc,CAAC,iBAAiB,EAAE;AAC1D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,kBAAkB,EAAE,CAAC;AACtE,CAAC;;ACFM,MAAM,kBAAkB,SAASC,cAAS,CAAC;AAClD,IAAI,MAAM,SAAS,CAAC,OAAO,EAAE;AAC7B,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,CAAC,iBAAiB,EAAE,OAAO,CAAC,MAAM,CAAC,mDAAmD,CAAC,CAAC;AACzH,IAAI;AACJ,IAAI,MAAM,MAAM,GAAG;AACnB,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,qCAAqC,CAAC;AACvE,IAAI;AACJ;;;;;;;"}
package/dist/plugin.js ADDED
@@ -0,0 +1,27 @@
1
+ var capacitorVideoTranscoder = (function (exports, core) {
2
+ 'use strict';
3
+
4
+ const VideoTranscoder = core.registerPlugin('VideoTranscoder', {
5
+ web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.VideoTranscoderWeb()),
6
+ });
7
+
8
+ class VideoTranscoderWeb extends core.WebPlugin {
9
+ async transcode(options) {
10
+ throw this.unimplemented(`Cannot transcode ${options.source} on the web: it needs the platform's video encoder.`);
11
+ }
12
+ async cancel() {
13
+ throw this.unimplemented('cancel is not available on the web.');
14
+ }
15
+ }
16
+
17
+ var web = /*#__PURE__*/Object.freeze({
18
+ __proto__: null,
19
+ VideoTranscoderWeb: VideoTranscoderWeb
20
+ });
21
+
22
+ exports.VideoTranscoder = VideoTranscoder;
23
+
24
+ return exports;
25
+
26
+ })({}, capacitorExports);
27
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst VideoTranscoder = registerPlugin('VideoTranscoder', {\n web: () => import('./web').then((m) => new m.VideoTranscoderWeb()),\n});\nexport * from './definitions';\nexport { VideoTranscoder };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class VideoTranscoderWeb extends WebPlugin {\n async transcode(options) {\n throw this.unimplemented(`Cannot transcode ${options.source} on the web: it needs the platform's video encoder.`);\n }\n async cancel() {\n throw this.unimplemented('cancel is not available on the web.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,eAAe,GAAGA,mBAAc,CAAC,iBAAiB,EAAE;IAC1D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,kBAAkB,EAAE,CAAC;IACtE,CAAC;;ICFM,MAAM,kBAAkB,SAASC,cAAS,CAAC;IAClD,IAAI,MAAM,SAAS,CAAC,OAAO,EAAE;IAC7B,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,CAAC,iBAAiB,EAAE,OAAO,CAAC,MAAM,CAAC,mDAAmD,CAAC,CAAC;IACzH,IAAI;IACJ,IAAI,MAAM,MAAM,GAAG;IACnB,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,qCAAqC,CAAC;IACvE,IAAI;IACJ;;;;;;;;;;;"}
@@ -0,0 +1,22 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ // The transcoder without the Capacitor bridge, so that `swift test --package-path ios` runs its tests on a Mac: no simulator, no
5
+ // phone. AVAssetReader and AVAssetWriter are the same on macOS, so the end-to-end tests transcode real files here. The plugin
6
+ // itself is the package at the root of the repository, which builds these same sources.
7
+ let package = Package(
8
+ name: "VideoTranscoderCore",
9
+ platforms: [.iOS(.v15), .macOS(.v13)],
10
+ products: [
11
+ .library(name: "VideoTranscoderCore", targets: ["VideoTranscoderCore"])
12
+ ],
13
+ targets: [
14
+ .target(
15
+ name: "VideoTranscoderCore",
16
+ path: "Sources/VideoTranscoderCore"),
17
+ .testTarget(
18
+ name: "VideoTranscoderCoreTests",
19
+ dependencies: ["VideoTranscoderCore"],
20
+ path: "Tests/VideoTranscoderCoreTests")
21
+ ]
22
+ )
@@ -0,0 +1,61 @@
1
+ import CoreGraphics
2
+ import Foundation
3
+
4
+ public struct VideoSize: Equatable {
5
+ public let width: Int
6
+ public let height: Int
7
+
8
+ public init(width: Int, height: Int) {
9
+ self.width = width
10
+ self.height = height
11
+ }
12
+ }
13
+
14
+ /// The arithmetic of the output: its size, its rotation, its audio format. Pure, so it is tested without a video.
15
+ public enum Geometry {
16
+ /// The size that fits a video into `maxSize` on its longer side, keeping the aspect ratio and never scaling up. Both sides are
17
+ /// rounded down to an even number (H.264 4:2:0 halves them for the chroma planes), and are at least 2. Integer arithmetic, so
18
+ /// 1920x1080 into 1280 is exactly 1280x720 and not 1280x718 from a 719.999 rounded down.
19
+ public static func outputSize(width: Int, height: Int, maxSize: Int) -> VideoSize {
20
+ let w = Int64(max(abs(width), 1))
21
+ let h = Int64(max(abs(height), 1))
22
+ let longer = max(w, h)
23
+ let limit = Int64(max(maxSize, 2))
24
+ func fit(_ side: Int64) -> Int {
25
+ let scaled = longer > limit ? side * limit / longer : side
26
+ return Int(max(2, scaled / 2 * 2))
27
+ }
28
+ return VideoSize(width: fit(w), height: fit(h))
29
+ }
30
+
31
+ /// The clockwise rotation a track transform applies, in degrees: 0, 90, 180 or 270.
32
+ public static func rotationDegrees(of transform: CGAffineTransform) -> Int {
33
+ let degrees = Int((atan2(Double(transform.b), Double(transform.a)) * 180 / .pi).rounded())
34
+ return ((degrees % 360) + 360) % 360 / 90 * 90
35
+ }
36
+
37
+ /// A stored size as a player shows it, with the track's rotation applied.
38
+ public static func displayedSize(_ size: VideoSize, rotationDegrees: Int) -> VideoSize {
39
+ rotationDegrees % 180 == 0 ? size : VideoSize(width: size.height, height: size.width)
40
+ }
41
+
42
+ /// The sample rates the AAC encoder takes.
43
+ static let aacSampleRates: [Double] = [8_000, 11_025, 12_000, 16_000, 22_050, 24_000, 32_000, 44_100, 48_000]
44
+
45
+ /// The AAC format an audio track that is not AAC is encoded to: mono stays mono, stereo stays stereo, more channels are mixed
46
+ /// down to stereo; the sample rate is the source's when AAC has it, else the next one up, at most 48 kHz. The bitrate is left
47
+ /// to the encoder, which picks one that suits the sample rate: a fixed 128 kbit/s is refused at 16 kHz ("Cannot Encode Media").
48
+ public static func aacFormat(sourceChannels: Int, sourceSampleRate: Double) -> (channels: Int, sampleRate: Double) {
49
+ let channels = sourceChannels <= 1 ? 1 : 2
50
+ let sampleRate = aacSampleRates.first { $0 >= sourceSampleRate.rounded() } ?? 48_000
51
+ return (channels, sampleRate)
52
+ }
53
+
54
+ /// Progress from 0 to 1 for a position in a clip, held below 1 until the file is finished.
55
+ public static func progress(seconds: Double, of duration: Double) -> Double {
56
+ guard duration > 0, seconds.isFinite else {
57
+ return 0
58
+ }
59
+ return min(max(seconds / duration, 0), 0.99)
60
+ }
61
+ }
@@ -0,0 +1,107 @@
1
+ import CoreFoundation
2
+ import Foundation
3
+
4
+ /// The `code` a call is rejected with. The raw values are the ones `src/definitions.ts` documents.
5
+ public enum TranscodeErrorCode: String {
6
+ case invalidArgument = "INVALID_ARGUMENT"
7
+ case notFound = "NOT_FOUND"
8
+ case noVideo = "NO_VIDEO"
9
+ case busy = "BUSY"
10
+ case cancelled = "CANCELLED"
11
+ case failed = "FAILED"
12
+ }
13
+
14
+ public struct TranscodeError: Error, Equatable, CustomStringConvertible {
15
+ public let code: TranscodeErrorCode
16
+ public let message: String
17
+
18
+ public init(_ code: TranscodeErrorCode, _ message: String) {
19
+ self.code = code
20
+ self.message = message
21
+ }
22
+
23
+ public var description: String { "\(code.rawValue): \(message)" }
24
+ }
25
+
26
+ /// What a `transcode` call asked for, checked and with the defaults filled in.
27
+ public struct TranscodeOptions: Equatable {
28
+ public static let defaultMaxSize = 1280
29
+ public static let defaultMaxDurationMs = 30_000
30
+ public static let defaultVideoBitrate = 1_800_000
31
+ public static let minimumMaxSize = 16
32
+
33
+ public let source: URL
34
+ /// `nil` when the call named none: a new file in the temporary directory is used.
35
+ public let destination: URL?
36
+ public let maxSize: Int
37
+ public let maxDurationMs: Int
38
+ public let videoBitrate: Int
39
+
40
+ public init(
41
+ source: URL,
42
+ destination: URL? = nil,
43
+ maxSize: Int = TranscodeOptions.defaultMaxSize,
44
+ maxDurationMs: Int = TranscodeOptions.defaultMaxDurationMs,
45
+ videoBitrate: Int = TranscodeOptions.defaultVideoBitrate
46
+ ) {
47
+ self.source = source
48
+ self.destination = destination
49
+ self.maxSize = maxSize
50
+ self.maxDurationMs = maxDurationMs
51
+ self.videoBitrate = videoBitrate
52
+ }
53
+
54
+ /// The options of a call as the bridge hands them over. Anything wrong is an `INVALID_ARGUMENT` that says what.
55
+ public static func parse(_ raw: [String: Any]) throws -> TranscodeOptions {
56
+ guard let source = raw["source"] as? String, !source.isEmpty else {
57
+ throw TranscodeError(.invalidArgument, "source is required: an absolute path or a file:// URL")
58
+ }
59
+ var destination: URL?
60
+ if let value = raw["destination"], !(value is NSNull) {
61
+ guard let text = value as? String else {
62
+ throw TranscodeError(.invalidArgument, "destination must be a string")
63
+ }
64
+ destination = try fileURL(text, name: "destination")
65
+ }
66
+ return TranscodeOptions(
67
+ source: try fileURL(source, name: "source"),
68
+ destination: destination,
69
+ maxSize: try wholeNumber(raw["maxSize"], name: "maxSize", minimum: minimumMaxSize, fallback: defaultMaxSize),
70
+ maxDurationMs: try wholeNumber(raw["maxDurationMs"], name: "maxDurationMs", minimum: 1, fallback: defaultMaxDurationMs),
71
+ videoBitrate: try wholeNumber(raw["videoBitrate"], name: "videoBitrate", minimum: 1, fallback: defaultVideoBitrate)
72
+ )
73
+ }
74
+
75
+ /// An absolute path or a `file://` URL as a file URL. Anything else (a relative path, `content://`, `http://`) is refused.
76
+ public static func fileURL(_ value: String, name: String) throws -> URL {
77
+ var path = value
78
+ if value.hasPrefix("file://") {
79
+ path = String(value.dropFirst("file://".count))
80
+ if path.hasPrefix("localhost/") {
81
+ path = String(path.dropFirst("localhost".count))
82
+ }
83
+ path = path.removingPercentEncoding ?? path
84
+ }
85
+ guard path.hasPrefix("/"), path.count > 1 else {
86
+ throw TranscodeError(.invalidArgument, "\(name) must be an absolute path or a file:// URL, not \(value)")
87
+ }
88
+ return URL(fileURLWithPath: path).standardizedFileURL
89
+ }
90
+
91
+ static func wholeNumber(_ value: Any?, name: String, minimum: Int, fallback: Int) throws -> Int {
92
+ guard let value, !(value is NSNull) else {
93
+ return fallback
94
+ }
95
+ guard let number = value as? NSNumber, CFGetTypeID(number) != CFBooleanGetTypeID() else {
96
+ throw TranscodeError(.invalidArgument, "\(name) must be a number")
97
+ }
98
+ let double = number.doubleValue
99
+ guard double.isFinite, double.rounded() == double, double <= Double(Int32.max) else {
100
+ throw TranscodeError(.invalidArgument, "\(name) must be a whole number, not \(number)")
101
+ }
102
+ guard Int(double) >= minimum else {
103
+ throw TranscodeError(.invalidArgument, "\(name) must be at least \(minimum), not \(number)")
104
+ }
105
+ return Int(double)
106
+ }
107
+ }