Skip to content

Commit 9b4ba2b

Browse files
authored
Add video analytics
1 parent 1f43db1 commit 9b4ba2b

27 files changed

+7914
-23660
lines changed

.travis.yml

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1+
dist: focal # Use Ubuntu 20.04 to get glibc >= 2.31
12
language: node_js
2-
before_script:
3-
- npm install
4-
before_install:
5-
- npm run
3+
node_js:
4+
- "18"
5+
6+
cache:
7+
directories:
8+
- ~/.npm
9+
10+
install:
11+
- npm ci # Use lockfile for consistent clean install
12+
613
script:
7-
- npm test
14+
- npm test # Run your tests
15+
816
after_success:
9-
- coverage
17+
- npm run coverage # Assumes you have a "coverage" script defined
18+
1019
notifications:
1120
email:
1221
recipients:
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const videoStarted = jest.fn();
2+
export const videoPlayed = jest.fn();
3+
export const videoPaused = jest.fn();
4+
export const videoCompleted = jest.fn();
5+
export const updateVideoDuration = jest.fn();
6+
7+
export const CloudinaryVideoAnalytics = jest.fn().mockImplementation((config: any) => ({
8+
config,
9+
videoStarted,
10+
videoPlayed,
11+
videoPaused,
12+
videoCompleted,
13+
updateVideoDuration,
14+
}));

example/App.tsx

Lines changed: 209 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
1-
import { StyleSheet, View } from 'react-native';
1+
import { StyleSheet, View, Text, TouchableOpacity, Alert, Platform, Dimensions } from 'react-native';
2+
import { StatusBar } from 'expo-status-bar';
23
import {AdvancedImage, AdvancedVideo} from 'cloudinary-react-native';
34
import {Cloudinary} from '@cloudinary/url-gen';
45
import {scale} from "@cloudinary/url-gen/actions/resize";
56
import {cartoonify} from "@cloudinary/url-gen/actions/effect";
67
import {max} from "@cloudinary/url-gen/actions/roundCorners";
7-
import React, {useRef} from "react";
8-
import { streamingProfile } from '@cloudinary/url-gen/actions/transcode';
9-
import { Video } from 'expo-av';
10-
import { } from 'cloudinary-react-native';
8+
import React, {useRef, useState} from "react";
9+
10+
const { height: screenHeight, width: screenWidth } = Dimensions.get('window');
11+
12+
// Calculate safe area padding based on screen dimensions
13+
const getTopPadding = () => {
14+
if (Platform.OS === 'ios') {
15+
// For iPhone X and newer (with notch), screen height is typically 812+ or width 390+
16+
if (screenHeight >= 812 || screenWidth >= 390) {
17+
return 60; // Devices with notch
18+
}
19+
return 40; // Older devices
20+
}
21+
return 35; // Android
22+
};
1123

1224
const cld = new Cloudinary({
1325
cloud: {
@@ -17,40 +29,212 @@ const cld = new Cloudinary({
1729
secure: true
1830
}
1931
});
32+
2033
export default function App() {
34+
const videoPlayer = useRef<any>(null);
35+
const [analyticsEnabled, setAnalyticsEnabled] = useState(false);
36+
const [autoTracking, setAutoTracking] = useState(false);
2137

22-
const videoPlayer = useRef<Video>(null);
2338
function createMyImage() {
2439
var myImage = cld.image('sample').resize(scale().width(300)).effect(cartoonify()).roundCorners(max());
2540
return myImage
2641
}
2742

2843
function createMyVideoObject() {
29-
const myVideo = cld.video('sea_turtle.m3u8').transcode(streamingProfile("auto"))
44+
const myVideo = cld.video('sea_turtle')
3045
return myVideo
3146
};
3247

48+
const toggleAnalytics = () => {
49+
const newAnalyticsState = !analyticsEnabled;
50+
setAnalyticsEnabled(newAnalyticsState);
51+
52+
// Auto-enable tracking when analytics are enabled for better UX
53+
if (newAnalyticsState && !autoTracking) {
54+
setAutoTracking(true);
55+
}
56+
57+
Alert.alert(
58+
'Analytics',
59+
`Analytics ${newAnalyticsState ? 'enabled' : 'disabled'}.${newAnalyticsState && !autoTracking ? ' Auto tracking also enabled.' : ''} Reload the video to see changes.`
60+
);
61+
};
62+
63+
const toggleAutoTracking = () => {
64+
setAutoTracking(!autoTracking);
65+
Alert.alert(
66+
'Auto Tracking',
67+
`Auto tracking ${!autoTracking ? 'enabled' : 'disabled'}. Reload the video to see changes.`
68+
);
69+
};
70+
71+
const startManualTracking = () => {
72+
if (videoPlayer.current && videoPlayer.current.startAnalyticsTracking) {
73+
videoPlayer.current.startAnalyticsTracking(
74+
{
75+
publicId: 'jnwczzoacujqb4r4loj1',
76+
cloudName: 'mobiledemoapp',
77+
type: 'video'
78+
},
79+
{
80+
customData: {
81+
userId: 'test-user-123',
82+
sessionId: 'test-session-456',
83+
category: 'demo-video'
84+
}
85+
}
86+
);
87+
Alert.alert('Manual Tracking', 'Manual analytics tracking started!');
88+
} else {
89+
Alert.alert('Error', 'Video ref not available or analytics not enabled');
90+
}
91+
};
92+
93+
const stopManualTracking = () => {
94+
if (videoPlayer.current && videoPlayer.current.stopAnalyticsTracking) {
95+
videoPlayer.current.stopAnalyticsTracking();
96+
Alert.alert('Manual Tracking', 'Manual analytics tracking stopped!');
97+
} else {
98+
Alert.alert('Error', 'Video ref not available');
99+
}
100+
};
101+
102+
const startAutoTrackingManually = () => {
103+
if (videoPlayer.current && videoPlayer.current.startAutoAnalyticsTracking) {
104+
videoPlayer.current.startAutoAnalyticsTracking({
105+
customData: {
106+
userId: 'test-user-123',
107+
source: 'manual-trigger'
108+
}
109+
});
110+
Alert.alert('Auto Tracking', 'Auto analytics tracking started manually!');
111+
} else {
112+
Alert.alert('Error', 'Video ref not available or analytics not enabled');
113+
}
114+
};
115+
116+
const addCustomEventToVideo = () => {
117+
if (videoPlayer.current && videoPlayer.current.addCustomEvent) {
118+
videoPlayer.current.addCustomEvent('user_interaction', {
119+
action: 'button_clicked',
120+
buttonName: 'share',
121+
videoPosition: 30.5, // seconds
122+
customData: {
123+
userId: 'demo-user-123',
124+
sessionId: 'session-456'
125+
}
126+
});
127+
Alert.alert('Custom Event', 'Custom analytics event sent!');
128+
} else {
129+
Alert.alert('Error', 'Custom events not available');
130+
}
131+
};
132+
33133
return (
34-
<View style={styles.container}>
35-
<View>
36-
<AdvancedImage cldImg={createMyImage()} style={{backgroundColor:"black", width:300, height:200}}/>
37-
</View>
38-
<View style={styles.videoContainer}>
39-
<AdvancedVideo
40-
ref={videoPlayer}
41-
videoStyle={styles.video}
42-
cldVideo={createMyVideoObject()}
43-
/>
134+
<View style={styles.safeArea}>
135+
<StatusBar style="auto" />
136+
<View style={styles.container}>
137+
<View>
138+
<AdvancedImage cldImg={createMyImage()} style={{backgroundColor:"black", width:300, height:200}}/>
139+
</View>
140+
141+
{/* Analytics Controls */}
142+
<View style={styles.controlsContainer}>
143+
<Text style={styles.title}>Analytics Testing</Text>
144+
145+
<TouchableOpacity style={styles.button} onPress={toggleAnalytics}>
146+
<Text style={styles.buttonText}>
147+
{analyticsEnabled ? 'Disable Analytics' : 'Enable Analytics'}
148+
</Text>
149+
</TouchableOpacity>
150+
151+
<TouchableOpacity style={styles.button} onPress={toggleAutoTracking}>
152+
<Text style={styles.buttonText}>
153+
{autoTracking ? 'Disable Auto Tracking' : 'Enable Auto Tracking'}
154+
</Text>
155+
</TouchableOpacity>
156+
157+
<TouchableOpacity style={styles.button} onPress={addCustomEventToVideo}>
158+
<Text style={styles.buttonText}>Send Custom Event</Text>
159+
</TouchableOpacity>
160+
</View>
161+
162+
<View style={styles.videoContainer}>
163+
<AdvancedVideo
164+
ref={videoPlayer}
165+
videoStyle={styles.video}
166+
cldVideo={createMyVideoObject()}
167+
enableAnalytics={analyticsEnabled}
168+
autoTrackAnalytics={autoTracking}
169+
analyticsOptions={{
170+
customData: {
171+
userId: 'demo-user-123',
172+
appVersion: '1.0.0',
173+
platform: 'react-native'
174+
},
175+
videoPlayerType: 'expo-av',
176+
videoPlayerVersion: '14.0.0'
177+
}}
178+
/>
179+
</View>
180+
181+
{/* Status Display */}
182+
<View style={styles.statusContainer}>
183+
<Text style={styles.statusText}>
184+
Analytics: {analyticsEnabled ? '✅ Enabled' : '❌ Disabled'}
185+
</Text>
186+
<Text style={styles.statusText}>
187+
Auto Tracking: {autoTracking ? '✅ Enabled' : '❌ Disabled'}
188+
</Text>
189+
</View>
44190
</View>
45191
</View>
46192
);
47193
}
48194

49195
const styles = StyleSheet.create({
196+
safeArea: {
197+
flex: 1,
198+
backgroundColor: '#fff',
199+
paddingTop: getTopPadding(),
200+
},
50201
container: {
51202
flex: 1,
52203
alignItems: 'center',
53204
justifyContent: 'center',
205+
paddingHorizontal: 20,
206+
},
207+
controlsContainer: {
208+
width: '90%',
209+
alignItems: 'center',
210+
marginVertical: 20,
211+
},
212+
title: {
213+
fontSize: 18,
214+
fontWeight: 'bold',
215+
marginBottom: 15,
216+
},
217+
button: {
218+
backgroundColor: '#007AFF',
219+
paddingHorizontal: 20,
220+
paddingVertical: 10,
221+
borderRadius: 8,
222+
marginVertical: 5,
223+
minWidth: 200,
224+
alignItems: 'center',
225+
},
226+
smallButton: {
227+
minWidth: 90,
228+
marginHorizontal: 5,
229+
},
230+
buttonRow: {
231+
flexDirection: 'row',
232+
justifyContent: 'center',
233+
},
234+
buttonText: {
235+
color: 'white',
236+
fontSize: 14,
237+
fontWeight: '600',
54238
},
55239
videoContainer: {
56240
width: '100%',
@@ -62,4 +246,12 @@ const styles = StyleSheet.create({
62246
width: 400,
63247
height: 220,
64248
},
249+
statusContainer: {
250+
marginTop: 20,
251+
alignItems: 'center',
252+
},
253+
statusText: {
254+
fontSize: 14,
255+
marginVertical: 2,
256+
},
65257
});

example/metro.config.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@ config.resolver.extraNodeModules = {
1818
'cloudinary-react-native': path.resolve(workspaceRoot),
1919
};
2020

21+
// Enhanced resolver configuration to handle @babel/runtime issues
22+
config.resolver.resolverMainFields = ['react-native', 'browser', 'main'];
23+
config.resolver.platforms = ['ios', 'android', 'native', 'web'];
24+
25+
// Fix for "./construct.js" error with proper path resolution
26+
config.resolver.alias = {
27+
...config.resolver.alias,
28+
'./construct.js': path.resolve(__dirname, 'node_modules/@babel/runtime/helpers/construct.js'),
29+
'./construct': path.resolve(__dirname, 'node_modules/@babel/runtime/helpers/construct.js'),
30+
};
31+
32+
// Additional resolver options for better module resolution
33+
config.resolver.sourceExts = ['js', 'jsx', 'ts', 'tsx', 'json'];
34+
2135
// 🚫 Exclude nested react-native versions
2236
config.resolver.blockList = exclusionList([
2337
new RegExp(`${workspaceRoot}/node_modules/react-native/.*`),

0 commit comments

Comments
 (0)