unified-video-framework 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/.github/workflows/ci.yml +253 -0
- package/ANDROID_TV_IMPLEMENTATION.md +313 -0
- package/COMPLETION_STATUS.md +165 -0
- package/CONTRIBUTING.md +376 -0
- package/FINAL_STATUS_REPORT.md +170 -0
- package/FRAMEWORK_REVIEW.md +247 -0
- package/IMPROVEMENTS_SUMMARY.md +168 -0
- package/LICENSE +21 -0
- package/NATIVE_APP_INTEGRATION_GUIDE.md +903 -0
- package/PAYWALL_RENTAL_FLOW.md +499 -0
- package/PLATFORM_SETUP_GUIDE.md +1636 -0
- package/README.md +315 -0
- package/RUN_LOCALLY.md +151 -0
- package/apps/demo/cast-sender-min.html +173 -0
- package/apps/demo/custom-player.html +883 -0
- package/apps/demo/demo.html +990 -0
- package/apps/demo/enhanced-player.html +3556 -0
- package/apps/demo/index.html +159 -0
- package/apps/rental-api/.env.example +24 -0
- package/apps/rental-api/README.md +23 -0
- package/apps/rental-api/migrations/001_init.sql +35 -0
- package/apps/rental-api/migrations/002_videos.sql +10 -0
- package/apps/rental-api/migrations/003_add_gateway_subref.sql +4 -0
- package/apps/rental-api/migrations/004_update_gateways.sql +4 -0
- package/apps/rental-api/migrations/005_seed_demo_video.sql +5 -0
- package/apps/rental-api/package-lock.json +2045 -0
- package/apps/rental-api/package.json +33 -0
- package/apps/rental-api/scripts/run-migration.js +42 -0
- package/apps/rental-api/scripts/update-video-currency.js +21 -0
- package/apps/rental-api/scripts/update-video-price.js +19 -0
- package/apps/rental-api/src/config.ts +14 -0
- package/apps/rental-api/src/db.ts +10 -0
- package/apps/rental-api/src/routes/cashfree.ts +167 -0
- package/apps/rental-api/src/routes/pesapal.ts +92 -0
- package/apps/rental-api/src/routes/rentals.ts +242 -0
- package/apps/rental-api/src/routes/webhooks.ts +73 -0
- package/apps/rental-api/src/server.ts +41 -0
- package/apps/rental-api/src/services/entitlements.ts +45 -0
- package/apps/rental-api/src/services/payments.ts +22 -0
- package/apps/rental-api/tsconfig.json +17 -0
- package/check-urls.ps1 +74 -0
- package/comparison-report.md +181 -0
- package/docs/PAYWALL.md +95 -0
- package/docs/PLAYER_UI_VISIBILITY.md +431 -0
- package/docs/README.md +7 -0
- package/docs/SYSTEM_ARCHITECTURE.md +612 -0
- package/docs/VDOCIPHER_CLONE_REQUIREMENTS.md +403 -0
- package/examples/android/JavaSampleApp/MainActivity.java +641 -0
- package/examples/android/JavaSampleApp/activity_main.xml +226 -0
- package/examples/android/SampleApp/MainActivity.kt +430 -0
- package/examples/ios/SampleApp/ViewController.swift +337 -0
- package/examples/ios/SwiftUISampleApp/ContentView.swift +304 -0
- package/iOS_IMPLEMENTATION_OPTIONS.md +470 -0
- package/ios/UnifiedVideoPlayer/UnifiedVideoPlayer.podspec +33 -0
- package/jest.config.js +33 -0
- package/jitpack.yml +5 -0
- package/lerna.json +35 -0
- package/package.json +69 -0
- package/packages/PLATFORM_STATUS.md +163 -0
- package/packages/android/build.gradle +135 -0
- package/packages/android/src/main/AndroidManifest.xml +36 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/PlayerConfiguration.java +221 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.java +1037 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.kt +707 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/analytics/AnalyticsProvider.java +9 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastManager.java +141 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastOptionsProvider.java +29 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/overlay/WatermarkOverlayView.java +88 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/pip/PipActionReceiver.java +33 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlaybackService.java +110 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlayerHolder.java +19 -0
- package/packages/core/package.json +34 -0
- package/packages/core/src/BasePlayer.ts +250 -0
- package/packages/core/src/VideoPlayer.ts +237 -0
- package/packages/core/src/VideoPlayerFactory.ts +145 -0
- package/packages/core/src/index.ts +20 -0
- package/packages/core/src/interfaces/IVideoPlayer.ts +184 -0
- package/packages/core/src/interfaces.ts +240 -0
- package/packages/core/src/utils/EventEmitter.ts +66 -0
- package/packages/core/src/utils/PlatformDetector.ts +300 -0
- package/packages/core/tsconfig.json +20 -0
- package/packages/enact/package.json +51 -0
- package/packages/enact/src/VideoPlayer.js +365 -0
- package/packages/enact/src/adapters/TizenAdapter.js +354 -0
- package/packages/enact/src/index.js +82 -0
- package/packages/ios/BUILD_INSTRUCTIONS.md +108 -0
- package/packages/ios/FIX_EMBED_ISSUE.md +142 -0
- package/packages/ios/GETTING_STARTED.md +100 -0
- package/packages/ios/Package.swift +35 -0
- package/packages/ios/README.md +84 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Analytics/AnalyticsEmitter.swift +26 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/DRM/FairPlayDRMManager.swift +102 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Info.plist +24 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Remote/RemoteCommandCenter.swift +109 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayer.swift +811 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayerView.swift +640 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Utilities/Color+Hex.swift +36 -0
- package/packages/ios/UnifiedVideoPlayer.podspec +27 -0
- package/packages/ios/UnifiedVideoPlayer.xcodeproj/project.pbxproj +385 -0
- package/packages/ios/build_framework.sh +55 -0
- package/packages/react-native/android/src/main/java/com/unifiedvideo/UnifiedVideoPlayerModule.kt +482 -0
- package/packages/react-native/ios/UnifiedVideoPlayer.swift +436 -0
- package/packages/react-native/package.json +51 -0
- package/packages/react-native/src/ReactNativePlayer.tsx +423 -0
- package/packages/react-native/src/VideoPlayer.tsx +224 -0
- package/packages/react-native/src/index.ts +28 -0
- package/packages/react-native/src/utils/EventEmitter.ts +66 -0
- package/packages/react-native/tsconfig.json +31 -0
- package/packages/roku/components/UnifiedVideoPlayer.brs +400 -0
- package/packages/roku/package.json +44 -0
- package/packages/roku/source/VideoPlayer.brs +231 -0
- package/packages/roku/source/main.brs +28 -0
- package/packages/web/GETTING_STARTED.md +292 -0
- package/packages/web/jest.config.js +28 -0
- package/packages/web/jest.setup.ts +110 -0
- package/packages/web/package.json +50 -0
- package/packages/web/src/SecureVideoPlayer.ts +1164 -0
- package/packages/web/src/WebPlayer.ts +3110 -0
- package/packages/web/src/__tests__/WebPlayer.test.ts +314 -0
- package/packages/web/src/index.ts +14 -0
- package/packages/web/src/paywall/PaywallController.ts +215 -0
- package/packages/web/src/react/WebPlayerView.tsx +177 -0
- package/packages/web/tsconfig.json +23 -0
- package/packages/web/webpack.config.js +45 -0
- package/server.js +131 -0
- package/server.py +84 -0
- package/test-urls.ps1 +97 -0
- package/test-video-urls.ps1 +87 -0
- package/tsconfig.json +39 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
package com.unifiedvideo.player.cast;
|
|
2
|
+
|
|
3
|
+
import android.net.Uri;
|
|
4
|
+
import android.util.Log;
|
|
5
|
+
|
|
6
|
+
import com.google.android.gms.cast.MediaInfo;
|
|
7
|
+
import com.google.android.gms.cast.MediaLoadRequestData;
|
|
8
|
+
import com.google.android.gms.cast.MediaMetadata;
|
|
9
|
+
import com.google.android.gms.cast.framework.CastContext;
|
|
10
|
+
import com.google.android.gms.cast.framework.CastSession;
|
|
11
|
+
import com.google.android.gms.cast.framework.SessionManagerListener;
|
|
12
|
+
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
|
13
|
+
|
|
14
|
+
import java.util.ArrayList;
|
|
15
|
+
import java.util.List;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Minimal Cast manager to load media on a remote receiver.
|
|
19
|
+
* Host app is expected to provide a Cast button via CastButtonFactory.
|
|
20
|
+
*/
|
|
21
|
+
public class CastManager {
|
|
22
|
+
private static final String TAG = "UVF-CastManager";
|
|
23
|
+
|
|
24
|
+
private final CastContext castContext;
|
|
25
|
+
|
|
26
|
+
public CastManager() {
|
|
27
|
+
this.castContext = CastContext.getSharedInstance();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public boolean hasSession() {
|
|
31
|
+
CastSession session = castContext.getSessionManager().getCurrentCastSession();
|
|
32
|
+
return session != null && session.isConnected();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public void startCasting(String url, String contentType, String title, List<SubtitleItem> subtitles, String activeSubtitleLabel) {
|
|
36
|
+
try {
|
|
37
|
+
CastSession session = castContext.getSessionManager().getCurrentCastSession();
|
|
38
|
+
if (session == null) {
|
|
39
|
+
Log.w(TAG, "No Cast session available");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
RemoteMediaClient rmc = session.getRemoteMediaClient();
|
|
43
|
+
if (rmc == null) {
|
|
44
|
+
Log.w(TAG, "RemoteMediaClient is null");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
MediaMetadata md = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
|
49
|
+
md.putString(MediaMetadata.KEY_TITLE, title != null ? title : "Unified Player");
|
|
50
|
+
|
|
51
|
+
MediaInfo.Builder mediaInfo = new MediaInfo.Builder(url)
|
|
52
|
+
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
|
53
|
+
.setContentType(contentType)
|
|
54
|
+
.setMetadata(md);
|
|
55
|
+
|
|
56
|
+
// Subtitles
|
|
57
|
+
if (subtitles != null && !subtitles.isEmpty()) {
|
|
58
|
+
List<com.google.android.gms.cast.media.Track> tracks = new ArrayList<>();
|
|
59
|
+
long nextId = 1;
|
|
60
|
+
Long activeId = null;
|
|
61
|
+
for (SubtitleItem sub : subtitles) {
|
|
62
|
+
com.google.android.gms.cast.media.Track t =
|
|
63
|
+
new com.google.android.gms.cast.media.Track.Builder(nextId, com.google.android.gms.cast.media.Track.TYPE_TEXT)
|
|
64
|
+
.setSubtype(com.google.android.gms.cast.media.Track.SUBTYPE_SUBTITLES)
|
|
65
|
+
.setContentId(sub.url)
|
|
66
|
+
.setContentType(sub.contentType != null ? sub.contentType : inferSubtitleContentType(sub.url))
|
|
67
|
+
.setLanguage(sub.language)
|
|
68
|
+
.setName(sub.label != null ? sub.label : sub.language)
|
|
69
|
+
.build();
|
|
70
|
+
if (sub.label != null && sub.label.equalsIgnoreCase(activeSubtitleLabel)) {
|
|
71
|
+
activeId = nextId;
|
|
72
|
+
}
|
|
73
|
+
tracks.add(t);
|
|
74
|
+
nextId++;
|
|
75
|
+
}
|
|
76
|
+
mediaInfo.setMediaTracks(tracks);
|
|
77
|
+
|
|
78
|
+
MediaLoadRequestData.Builder load = new MediaLoadRequestData.Builder()
|
|
79
|
+
.setMediaInfo(mediaInfo.build())
|
|
80
|
+
.setAutoplay(true);
|
|
81
|
+
if (activeId != null) {
|
|
82
|
+
List<Long> activeTrackIds = new ArrayList<>();
|
|
83
|
+
activeTrackIds.add(activeId);
|
|
84
|
+
load.setActiveTrackIds(activeTrackIds);
|
|
85
|
+
}
|
|
86
|
+
rmc.load(load.build());
|
|
87
|
+
} else {
|
|
88
|
+
rmc.load(new MediaLoadRequestData.Builder()
|
|
89
|
+
.setMediaInfo(mediaInfo.build())
|
|
90
|
+
.setAutoplay(true)
|
|
91
|
+
.build());
|
|
92
|
+
}
|
|
93
|
+
} catch (Exception e) {
|
|
94
|
+
Log.e(TAG, "Cast load failed", e);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public void stopCasting() {
|
|
99
|
+
try {
|
|
100
|
+
CastSession session = castContext.getSessionManager().getCurrentCastSession();
|
|
101
|
+
if (session != null) {
|
|
102
|
+
RemoteMediaClient rmc = session.getRemoteMediaClient();
|
|
103
|
+
if (rmc != null) {
|
|
104
|
+
rmc.stop();
|
|
105
|
+
}
|
|
106
|
+
castContext.getSessionManager().endCurrentSession(true);
|
|
107
|
+
}
|
|
108
|
+
} catch (Exception ignored) {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public void addSessionManagerListener(SessionManagerListener<CastSession> listener) {
|
|
112
|
+
try { castContext.getSessionManager().addSessionManagerListener(listener, CastSession.class); } catch (Exception ignored) {}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public void removeSessionManagerListener(SessionManagerListener<CastSession> listener) {
|
|
116
|
+
try { castContext.getSessionManager().removeSessionManagerListener(listener, CastSession.class); } catch (Exception ignored) {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private String inferSubtitleContentType(String url) {
|
|
120
|
+
String u = url != null ? url.toLowerCase() : "";
|
|
121
|
+
if (u.endsWith(".vtt")) return "text/vtt";
|
|
122
|
+
if (u.endsWith(".srt")) return "application/x-subrip";
|
|
123
|
+
if (u.endsWith(".ttml") || u.endsWith(".dfxp") || u.endsWith(".xml")) return "application/ttml+xml";
|
|
124
|
+
return "text/vtt";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public static class SubtitleItem {
|
|
128
|
+
public final String url;
|
|
129
|
+
public final String language;
|
|
130
|
+
public final String label;
|
|
131
|
+
public final String contentType;
|
|
132
|
+
|
|
133
|
+
public SubtitleItem(String url, String language, String label, String contentType) {
|
|
134
|
+
this.url = url;
|
|
135
|
+
this.language = language;
|
|
136
|
+
this.label = label;
|
|
137
|
+
this.contentType = contentType;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package com.unifiedvideo.player.cast;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
|
|
5
|
+
import com.google.android.gms.cast.framework.CastOptions;
|
|
6
|
+
import com.google.android.gms.cast.framework.OptionsProvider;
|
|
7
|
+
import com.google.android.gms.cast.framework.SessionProvider;
|
|
8
|
+
|
|
9
|
+
import java.util.List;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Provides CastOptions to the Cast framework. Referenced from AndroidManifest.
|
|
13
|
+
*/
|
|
14
|
+
public class CastOptionsProvider implements OptionsProvider {
|
|
15
|
+
@Override
|
|
16
|
+
public CastOptions getCastOptions(Context context) {
|
|
17
|
+
// Use default media receiver; apps can replace via setter in CastManager
|
|
18
|
+
String receiverAppId = com.google.android.gms.cast.framework.media.CastMediaOptions.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
|
|
19
|
+
return new CastOptions.Builder()
|
|
20
|
+
.setReceiverApplicationId(receiverAppId)
|
|
21
|
+
.build();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Override
|
|
25
|
+
public List<SessionProvider> getAdditionalSessionProviders(Context context) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
package/packages/android/src/main/java/com/unifiedvideo/player/overlay/WatermarkOverlayView.java
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
package com.unifiedvideo.player.overlay;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.graphics.Canvas;
|
|
5
|
+
import android.graphics.LinearGradient;
|
|
6
|
+
import android.graphics.Paint;
|
|
7
|
+
import android.graphics.Shader;
|
|
8
|
+
import android.util.AttributeSet;
|
|
9
|
+
import android.view.View;
|
|
10
|
+
|
|
11
|
+
import java.util.Random;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Semi-random moving watermark overlay similar to web watermark.
|
|
15
|
+
*/
|
|
16
|
+
public class WatermarkOverlayView extends View {
|
|
17
|
+
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
18
|
+
private final Random random = new Random();
|
|
19
|
+
|
|
20
|
+
private int colorStart = 0xFFFF0000; // #ff0000
|
|
21
|
+
private int colorEnd = 0xFFFF4D4F; // #ff4d4f
|
|
22
|
+
private float alpha = 0.3f;
|
|
23
|
+
|
|
24
|
+
private String text = "PREMIUM";
|
|
25
|
+
private float x = 50;
|
|
26
|
+
private float y = 80;
|
|
27
|
+
|
|
28
|
+
public WatermarkOverlayView(Context context) {
|
|
29
|
+
super(context);
|
|
30
|
+
init();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public WatermarkOverlayView(Context context, AttributeSet attrs) {
|
|
34
|
+
super(context, attrs);
|
|
35
|
+
init();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public WatermarkOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
39
|
+
super(context, attrs, defStyleAttr);
|
|
40
|
+
init();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private void init() {
|
|
44
|
+
paint.setTextSize(42f);
|
|
45
|
+
paint.setStyle(Paint.Style.FILL);
|
|
46
|
+
setClickable(false);
|
|
47
|
+
setFocusable(false);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public void setAccentColors(int startColor, int endColor) {
|
|
51
|
+
this.colorStart = startColor;
|
|
52
|
+
this.colorEnd = endColor;
|
|
53
|
+
invalidate();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public void setAlphaFactor(float alphaFactor) {
|
|
57
|
+
this.alpha = Math.max(0f, Math.min(1f, alphaFactor));
|
|
58
|
+
invalidate();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public void setText(String text) {
|
|
62
|
+
this.text = text;
|
|
63
|
+
invalidate();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Move watermark to a random position and redraw. */
|
|
67
|
+
public void randomize() {
|
|
68
|
+
int w = getWidth();
|
|
69
|
+
int h = getHeight();
|
|
70
|
+
if (w <= 0 || h <= 0) return;
|
|
71
|
+
float margin = 20f;
|
|
72
|
+
x = margin + random.nextFloat() * Math.max(1f, (w - margin * 2 - 200));
|
|
73
|
+
y = margin + random.nextFloat() * Math.max(1f, (h - margin * 2 - 60));
|
|
74
|
+
invalidate();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Override
|
|
78
|
+
protected void onDraw(Canvas canvas) {
|
|
79
|
+
super.onDraw(canvas);
|
|
80
|
+
int w = getWidth();
|
|
81
|
+
Shader shader = new LinearGradient(0, 0, w, 0, colorStart, colorEnd, Shader.TileMode.CLAMP);
|
|
82
|
+
paint.setShader(shader);
|
|
83
|
+
paint.setAlpha((int) (alpha * 255));
|
|
84
|
+
String content = text + " • " + System.currentTimeMillis() % 100000;
|
|
85
|
+
canvas.drawText(content, x, y, paint);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.unifiedvideo.player.pip;
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver;
|
|
4
|
+
import android.content.Context;
|
|
5
|
+
import android.content.Intent;
|
|
6
|
+
import android.util.Log;
|
|
7
|
+
|
|
8
|
+
import com.google.android.exoplayer2.ExoPlayer;
|
|
9
|
+
import com.unifiedvideo.player.services.PlayerHolder;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Receives PiP action intents to control playback.
|
|
13
|
+
*/
|
|
14
|
+
public class PipActionReceiver extends BroadcastReceiver {
|
|
15
|
+
public static final String ACTION_PLAY = "com.unifiedvideo.player.ACTION_PLAY";
|
|
16
|
+
public static final String ACTION_PAUSE = "com.unifiedvideo.player.ACTION_PAUSE";
|
|
17
|
+
|
|
18
|
+
@Override
|
|
19
|
+
public void onReceive(Context context, Intent intent) {
|
|
20
|
+
if (intent == null || intent.getAction() == null) return;
|
|
21
|
+
ExoPlayer player = PlayerHolder.getPlayer();
|
|
22
|
+
if (player == null) return;
|
|
23
|
+
switch (intent.getAction()) {
|
|
24
|
+
case ACTION_PLAY:
|
|
25
|
+
try { player.play(); } catch (Exception e) { Log.w("PipAction", "play failed", e); }
|
|
26
|
+
break;
|
|
27
|
+
case ACTION_PAUSE:
|
|
28
|
+
try { player.pause(); } catch (Exception e) { Log.w("PipAction", "pause failed", e); }
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
package com.unifiedvideo.player.services;
|
|
2
|
+
|
|
3
|
+
import android.app.Notification;
|
|
4
|
+
import android.app.NotificationChannel;
|
|
5
|
+
import android.app.NotificationManager;
|
|
6
|
+
import android.app.PendingIntent;
|
|
7
|
+
import android.app.Service;
|
|
8
|
+
import android.content.Context;
|
|
9
|
+
import android.content.Intent;
|
|
10
|
+
import android.os.Build;
|
|
11
|
+
import android.os.IBinder;
|
|
12
|
+
|
|
13
|
+
import androidx.annotation.Nullable;
|
|
14
|
+
import androidx.core.app.NotificationCompat;
|
|
15
|
+
import androidx.media.app.NotificationCompat.MediaStyle;
|
|
16
|
+
import android.support.v4.media.session.MediaSessionCompat;
|
|
17
|
+
import android.support.v4.media.session.PlaybackStateCompat;
|
|
18
|
+
|
|
19
|
+
import com.google.android.exoplayer2.ExoPlayer;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Foreground service for background playback. Minimal implementation.
|
|
23
|
+
*/
|
|
24
|
+
public class PlaybackService extends Service {
|
|
25
|
+
private static final String CHANNEL_ID = "uvf_playback";
|
|
26
|
+
private static final int NOTIFICATION_ID = 1001;
|
|
27
|
+
|
|
28
|
+
private MediaSessionCompat mediaSession;
|
|
29
|
+
|
|
30
|
+
@Override
|
|
31
|
+
public void onCreate() {
|
|
32
|
+
super.onCreate();
|
|
33
|
+
mediaSession = new MediaSessionCompat(this, "UVF-Player");
|
|
34
|
+
mediaSession.setActive(true);
|
|
35
|
+
createChannel();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Override
|
|
39
|
+
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
40
|
+
startForeground(NOTIFICATION_ID, buildNotification());
|
|
41
|
+
return START_STICKY;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Nullable
|
|
45
|
+
@Override
|
|
46
|
+
public IBinder onBind(Intent intent) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Override
|
|
51
|
+
public void onDestroy() {
|
|
52
|
+
super.onDestroy();
|
|
53
|
+
if (mediaSession != null) {
|
|
54
|
+
mediaSession.setActive(false);
|
|
55
|
+
mediaSession.release();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private Notification buildNotification() {
|
|
60
|
+
ExoPlayer player = PlayerHolder.getPlayer();
|
|
61
|
+
boolean isPlaying = player != null && player.isPlaying();
|
|
62
|
+
|
|
63
|
+
PlaybackStateCompat state = new PlaybackStateCompat.Builder()
|
|
64
|
+
.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE)
|
|
65
|
+
.setState(isPlaying ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED, player != null ? player.getCurrentPosition() : 0, 1.0f)
|
|
66
|
+
.build();
|
|
67
|
+
mediaSession.setPlaybackState(state);
|
|
68
|
+
|
|
69
|
+
PendingIntent playIntent = PendingIntent.getBroadcast(this, 100,
|
|
70
|
+
new Intent("com.unifiedvideo.player.ACTION_PLAY"), Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0);
|
|
71
|
+
PendingIntent pauseIntent = PendingIntent.getBroadcast(this, 101,
|
|
72
|
+
new Intent("com.unifiedvideo.player.ACTION_PAUSE"), Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0);
|
|
73
|
+
|
|
74
|
+
NotificationCompat.Action playAction = new NotificationCompat.Action(android.R.drawable.ic_media_play, "Play", playIntent);
|
|
75
|
+
NotificationCompat.Action pauseAction = new NotificationCompat.Action(android.R.drawable.ic_media_pause, "Pause", pauseIntent);
|
|
76
|
+
|
|
77
|
+
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
|
|
78
|
+
.setSmallIcon(android.R.drawable.ic_media_play)
|
|
79
|
+
.setContentTitle("Playing video")
|
|
80
|
+
.setContentText("Unified Video Player")
|
|
81
|
+
.setOngoing(isPlaying)
|
|
82
|
+
.addAction(isPlaying ? pauseAction : playAction)
|
|
83
|
+
.setStyle(new MediaStyle().setMediaSession(mediaSession.getSessionToken()).setShowActionsInCompactView(0));
|
|
84
|
+
|
|
85
|
+
return builder.build();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private void createChannel() {
|
|
89
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
90
|
+
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Playback", NotificationManager.IMPORTANCE_LOW);
|
|
91
|
+
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
|
92
|
+
nm.createNotificationChannel(channel);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Static helpers
|
|
97
|
+
public static void start(Context ctx) {
|
|
98
|
+
Intent i = new Intent(ctx, PlaybackService.class);
|
|
99
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
100
|
+
ctx.startForegroundService(i);
|
|
101
|
+
} else {
|
|
102
|
+
ctx.startService(i);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public static void stop(Context ctx) {
|
|
107
|
+
ctx.stopService(new Intent(ctx, PlaybackService.class));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package com.unifiedvideo.player.services;
|
|
2
|
+
|
|
3
|
+
import com.google.android.exoplayer2.ExoPlayer;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Holds a reference to the current ExoPlayer for background service and PiP actions.
|
|
7
|
+
*/
|
|
8
|
+
public class PlayerHolder {
|
|
9
|
+
private static volatile ExoPlayer player;
|
|
10
|
+
|
|
11
|
+
public static void setPlayer(ExoPlayer p) {
|
|
12
|
+
player = p;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public static ExoPlayer getPlayer() {
|
|
16
|
+
return player;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unified-video/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Core interfaces and factory for Unified Video Framework",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc -p tsconfig.json",
|
|
13
|
+
"watch": "tsc -p tsconfig.json --watch",
|
|
14
|
+
"clean": "rm -rf dist",
|
|
15
|
+
"test": "jest",
|
|
16
|
+
"lint": "eslint src --ext .ts,.tsx"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"video",
|
|
20
|
+
"player",
|
|
21
|
+
"framework",
|
|
22
|
+
"core",
|
|
23
|
+
"interfaces"
|
|
24
|
+
],
|
|
25
|
+
"author": "Your Company",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^18.0.0",
|
|
29
|
+
"typescript": "^4.9.0"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|