Main coves client
1import 'package:dio/dio.dart';
2import 'package:flutter/foundation.dart';
3
4/// Service for interacting with Streamable API
5///
6/// Fetches video data from Streamable to get direct MP4 URLs
7/// for in-app video playback.
8///
9/// Implements caching to reduce API calls for recently accessed videos.
10class StreamableService {
11 StreamableService({Dio? dio}) : _dio = dio ?? _sharedDio;
12
13 // Singleton Dio instance for efficient connection reuse
14 static final Dio _sharedDio = Dio(
15 BaseOptions(
16 connectTimeout: const Duration(seconds: 10),
17 receiveTimeout: const Duration(seconds: 10),
18 sendTimeout: const Duration(seconds: 10),
19 ),
20 );
21
22 final Dio _dio;
23
24 // Cache for video URLs (shortcode -> {url, timestamp})
25 // Short-lived cache (5 min) to reduce API calls
26 final Map<String, ({String url, DateTime cachedAt})> _urlCache = {};
27 static const Duration _cacheDuration = Duration(minutes: 5);
28
29 /// Extracts the Streamable shortcode from a URL
30 ///
31 /// Examples:
32 /// - https://streamable.com/7kpdft -> 7kpdft
33 /// - https://streamable.com/e/abc123 -> abc123
34 /// - streamable.com/abc123 -> abc123
35 static String? extractShortcode(String url) {
36 try {
37 // Handle URLs without scheme
38 var urlToParse = url;
39 if (!url.contains('://')) {
40 urlToParse = 'https://$url';
41 }
42
43 final uri = Uri.parse(urlToParse);
44 final path = uri.path;
45
46 // Get the last non-empty path segment (handles /e/ prefix and other cases)
47 final segments = path.split('/').where((s) => s.isNotEmpty).toList();
48 if (segments.isEmpty) {
49 return null;
50 }
51
52 final shortcode = segments.last;
53
54 if (shortcode.isEmpty) {
55 return null;
56 }
57
58 return shortcode;
59 } on FormatException catch (e) {
60 if (kDebugMode) {
61 debugPrint('Error extracting Streamable shortcode: $e');
62 }
63 return null;
64 }
65 }
66
67 /// Fetches the MP4 video URL for a Streamable video
68 ///
69 /// Returns the direct MP4 URL or null if the video cannot be fetched.
70 /// Uses a 5-minute cache to reduce API calls for repeated access.
71 Future<String?> getVideoUrl(String streamableUrl) async {
72 try {
73 final shortcode = extractShortcode(streamableUrl);
74 if (shortcode == null) {
75 if (kDebugMode) {
76 debugPrint('Failed to extract shortcode from: $streamableUrl');
77 }
78 return null;
79 }
80
81 // Check cache first
82 final cached = _urlCache[shortcode];
83 if (cached != null) {
84 final age = DateTime.now().difference(cached.cachedAt);
85 if (age < _cacheDuration) {
86 if (kDebugMode) {
87 debugPrint('Using cached URL for shortcode: $shortcode');
88 }
89 return cached.url;
90 }
91 // Cache expired, remove it
92 _urlCache.remove(shortcode);
93 }
94
95 // Fetch video data from Streamable API
96 final response = await _dio.get<Map<String, dynamic>>(
97 'https://api.streamable.com/videos/$shortcode',
98 );
99
100 if (response.statusCode == 200 && response.data != null) {
101 final data = response.data!;
102
103 // Extract MP4 URL from response
104 // Response structure: { "files": { "mp4": { "url": "//..." } } }
105 final files = data['files'] as Map<String, dynamic>?;
106 if (files == null) {
107 if (kDebugMode) {
108 debugPrint('No files found in Streamable response');
109 }
110 return null;
111 }
112
113 final mp4 = files['mp4'] as Map<String, dynamic>?;
114 if (mp4 == null) {
115 if (kDebugMode) {
116 debugPrint('No MP4 file found in Streamable response');
117 }
118 return null;
119 }
120
121 var videoUrl = mp4['url'] as String?;
122 if (videoUrl == null) {
123 if (kDebugMode) {
124 debugPrint('No URL found in MP4 data');
125 }
126 return null;
127 }
128
129 // Prepend https: if URL is protocol-relative
130 if (videoUrl.startsWith('//')) {
131 videoUrl = 'https:$videoUrl';
132 }
133
134 // Cache the URL for future requests
135 _urlCache[shortcode] = (url: videoUrl, cachedAt: DateTime.now());
136
137 return videoUrl;
138 }
139
140 if (kDebugMode) {
141 debugPrint('Failed to fetch Streamable video: ${response.statusCode}');
142 }
143 return null;
144 } on DioException catch (e) {
145 if (kDebugMode) {
146 debugPrint('Error fetching Streamable video URL: $e');
147 }
148 return null;
149 }
150 }
151}