[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: always transcode videos

+186 -64
+131 -9
lib/src/core/network/atproto/data/repositories/feed_repository_impl.dart
··· 1310 1310 'Video upload failed: ${response.statusCode} ${response.body}', 1311 1311 ); 1312 1312 throw VideoUploadException( 1313 - response.statusCode == 413 1314 - ? 'Video is too large to upload.' 1315 - : 'Failed to upload video.', 1313 + _buildVideoUploadFailureMessage( 1314 + fallback: response.statusCode == 413 1315 + ? 'Video is too large to upload.' 1316 + : 'Failed to upload video.', 1317 + detail: response.body, 1318 + ), 1316 1319 statusCode: response.statusCode, 1317 1320 uploadSizeBytes: videoSizeBytes, 1318 1321 limitBytes: maxUploadSizeBytes > 0 ? maxUploadSizeBytes : null, ··· 1337 1340 await Future.delayed(const Duration(seconds: 2)); 1338 1341 attempts++; 1339 1342 if (attempts > maxAttempts) { 1340 - throw Exception('Timed out waiting for video processing to finish'); 1343 + throw const VideoUploadException( 1344 + 'Video processing timed out. Please try again.', 1345 + ); 1341 1346 } 1342 1347 1343 1348 try { ··· 1379 1384 'Too many consecutive polling errors, giving up: $e', 1380 1385 error: e, 1381 1386 ); 1382 - throw Exception( 1383 - 'Failed to check video upload status after ' 1384 - '$maxConsecutivePollErrors attempts: $e', 1387 + throw VideoUploadException( 1388 + _buildVideoUploadFailureMessage( 1389 + fallback: 'Failed to check video processing status.', 1390 + detail: e.toString(), 1391 + ), 1392 + responseBody: e.toString(), 1385 1393 ); 1386 1394 } 1387 1395 ··· 1393 1401 } 1394 1402 1395 1403 if (responseData['jobStatus']?['state'] == 'JOB_STATE_FAILED') { 1396 - throw Exception( 1397 - 'Video upload failed: ${responseData['jobStatus']?['status']}', 1404 + final failureMessage = _buildVideoUploadFailureMessage( 1405 + fallback: 'Video processing failed.', 1406 + detail: responseData['jobStatus'] ?? responseData, 1407 + ); 1408 + _logger.e( 1409 + 'Video processing job failed: $failureMessage', 1410 + error: responseData, 1411 + ); 1412 + throw VideoUploadException( 1413 + failureMessage, 1414 + responseBody: jsonEncode(responseData), 1398 1415 ); 1399 1416 } 1400 1417 ··· 1974 1991 default: 1975 1992 return 'video/mp4'; // Default to mp4 1976 1993 } 1994 + } 1995 + 1996 + String _buildVideoUploadFailureMessage({ 1997 + required String fallback, 1998 + dynamic detail, 1999 + }) { 2000 + final normalizedDetail = _extractVideoUploadFailureDetail(detail); 2001 + if (normalizedDetail == null) { 2002 + return fallback; 2003 + } 2004 + 2005 + final normalizedFallback = fallback.trim(); 2006 + if (normalizedDetail.toLowerCase() == normalizedFallback.toLowerCase()) { 2007 + return normalizedFallback; 2008 + } 2009 + if (normalizedDetail.toLowerCase().startsWith( 2010 + normalizedFallback.toLowerCase(), 2011 + )) { 2012 + return normalizedDetail; 2013 + } 2014 + 2015 + final separator = normalizedFallback.endsWith('.') ? ' ' : ': '; 2016 + return '$normalizedFallback$separator$normalizedDetail'; 2017 + } 2018 + 2019 + String? _extractVideoUploadFailureDetail(dynamic value) { 2020 + if (value == null) { 2021 + return null; 2022 + } 2023 + 2024 + if (value is String) { 2025 + final trimmed = value.trim(); 2026 + if (trimmed.isEmpty) { 2027 + return null; 2028 + } 2029 + 2030 + try { 2031 + final decoded = jsonDecode(trimmed); 2032 + final decodedDetail = _extractVideoUploadFailureDetail(decoded); 2033 + if (decodedDetail != null) { 2034 + return decodedDetail; 2035 + } 2036 + } catch (_) { 2037 + // Fall back to the raw string when the response is not JSON. 2038 + } 2039 + 2040 + return _sanitizeVideoUploadFailureText(trimmed); 2041 + } 2042 + 2043 + if (value is Map) { 2044 + for (final key in const [ 2045 + 'message', 2046 + 'status', 2047 + 'detail', 2048 + 'reason', 2049 + 'description', 2050 + 'error', 2051 + ]) { 2052 + final nestedDetail = _extractVideoUploadFailureDetail(value[key]); 2053 + if (nestedDetail != null) { 2054 + return nestedDetail; 2055 + } 2056 + } 2057 + 2058 + final jobStatusDetail = _extractVideoUploadFailureDetail( 2059 + value['jobStatus'], 2060 + ); 2061 + if (jobStatusDetail != null) { 2062 + return jobStatusDetail; 2063 + } 2064 + 2065 + return null; 2066 + } 2067 + 2068 + if (value is Iterable) { 2069 + for (final item in value) { 2070 + final itemDetail = _extractVideoUploadFailureDetail(item); 2071 + if (itemDetail != null) { 2072 + return itemDetail; 2073 + } 2074 + } 2075 + return null; 2076 + } 2077 + 2078 + return _sanitizeVideoUploadFailureText(value.toString()); 2079 + } 2080 + 2081 + String? _sanitizeVideoUploadFailureText(String text) { 2082 + final sanitized = text 2083 + .replaceFirst( 2084 + RegExp(r'^(exception|error):\s*', caseSensitive: false), 2085 + '', 2086 + ) 2087 + .replaceAll(RegExp(r'\s+'), ' ') 2088 + .trim(); 2089 + 2090 + if (sanitized.isEmpty || 2091 + sanitized == '{}' || 2092 + sanitized == '[]' || 2093 + sanitized.startsWith('<!DOCTYPE html') || 2094 + sanitized.startsWith('<html')) { 2095 + return null; 2096 + } 2097 + 2098 + return sanitized; 1977 2099 } 1978 2100 }
-54
lib/src/core/pro_video_editor/ui/video_editor_grounded_page.dart
··· 47 47 class _VideoEditorGroundedPageState extends State<VideoEditorGroundedPage> 48 48 with StoryMentionEditing<VideoEditorGroundedPage> { 49 49 static const _storyCanvasSize = Size(1440, 2560); 50 - static const _trimTolerance = Duration(milliseconds: 100); 51 50 static const _uploadCompressionMinFileSizeBytes = 25 * 1024 * 1024; 52 51 static const _uploadCompressionBitrate = 3000000; 53 52 static const _uploadCompressionMaxLongEdge = 1920.0; ··· 574 573 sourceVideoPath, 575 574 ); 576 575 577 - if (_canUseOriginalVideo(parameters) && !shouldCompressForUpload) { 578 - _outputPath = sourceVideoPath; 579 - return; 580 - } 581 - 582 576 final directory = await getTemporaryDirectory(); 583 577 584 578 double overlayVolume = 0; ··· 671 665 scaleX: (transform.scaleX ?? 1) * scale, 672 666 scaleY: (transform.scaleY ?? 1) * scale, 673 667 ); 674 - } 675 - 676 - bool _canUseOriginalVideo(CompleteParameters parameters) { 677 - if (parameters.layers.isNotEmpty || 678 - parameters.colorFilters.isNotEmpty || 679 - parameters.customAudioTrack != null || 680 - (parameters.blur).abs() > 0.001 || 681 - _hasTrim(parameters) || 682 - _hasRotation(parameters) || 683 - parameters.flipX || 684 - parameters.flipY || 685 - (_proVideoController?.isAudioEnabled == false)) { 686 - return false; 687 - } 688 - 689 - if (!widget.storyMode) { 690 - return !parameters.isTransformed; 691 - } 692 - 693 - return _isStoryExportTransformIdentity(); 694 - } 695 - 696 - bool _hasTrim(CompleteParameters parameters) { 697 - final startTime = 698 - _durationSpan?.start ?? parameters.startTime ?? Duration.zero; 699 - final endTime = 700 - _durationSpan?.end ?? parameters.endTime ?? _videoMetadata.duration; 701 - 702 - return startTime > _trimTolerance || 703 - _videoMetadata.duration - endTime > _trimTolerance; 704 - } 705 - 706 - bool _hasRotation(CompleteParameters parameters) { 707 - final normalizedTurns = parameters.rotateTurns % 4; 708 - return normalizedTurns != 0; 709 - } 710 - 711 - bool _isStoryExportTransformIdentity() { 712 - final coverCrop = _computeStoryCoverCrop(_videoMetadata.resolution); 713 - final sourceWidth = _videoMetadata.resolution.width.round(); 714 - final sourceHeight = _videoMetadata.resolution.height.round(); 715 - 716 - return coverCrop.x == 0 && 717 - coverCrop.y == 0 && 718 - coverCrop.width == sourceWidth && 719 - coverCrop.height == sourceHeight && 720 - _storyTargetWidth(coverCrop) == coverCrop.width && 721 - _storyTargetHeight(coverCrop) == coverCrop.height; 722 668 } 723 669 724 670 int? _targetExportBitrate(ExportTransform? transform) {
+30 -1
lib/src/core/utils/error_messages.dart
··· 21 21 } 22 22 return 'This video is too large to upload. Please trim or compress it and try again.'; 23 23 } 24 + 25 + final detailedMessage = _cleanErrorMessage(error.message); 26 + if (detailedMessage.isNotEmpty) { 27 + return detailedMessage; 28 + } 24 29 return 'Unable to upload video. Please try again'; 25 30 } 26 31 27 - final errorStr = error.toString().toLowerCase(); 32 + final rawMessage = _cleanErrorMessage(error.toString()); 33 + final errorStr = rawMessage.toLowerCase(); 28 34 29 35 // Upload size errors 30 36 if (errorStr.contains('413') || 31 37 errorStr.contains('payload too large') || 32 38 errorStr.contains('too large')) { 33 39 return 'This file is too large to upload. Please trim or compress it and try again.'; 40 + } 41 + 42 + if (_shouldSurfaceDetailedMessage(errorStr)) { 43 + return rawMessage; 34 44 } 35 45 36 46 // Network errors ··· 150 160 return '${(bytes / kb).toStringAsFixed(0)} KB'; 151 161 } 152 162 return '$bytes B'; 163 + } 164 + 165 + static String _cleanErrorMessage(String message) { 166 + return message 167 + .replaceFirst( 168 + RegExp(r'^(exception|error):\s*', caseSensitive: false), 169 + '', 170 + ) 171 + .trim(); 172 + } 173 + 174 + static bool _shouldSurfaceDetailedMessage(String message) { 175 + return message.startsWith('failed to upload video') || 176 + message.startsWith('video processing failed') || 177 + message.startsWith('failed to check video processing status') || 178 + message.startsWith('video processing timed out') || 179 + message.startsWith('timed out waiting for video processing') || 180 + message.startsWith('video file not found') || 181 + message.startsWith('video file is empty'); 153 182 } 154 183 }
+25
test/src/core/utils/error_messages_test.dart
··· 30 30 'This file is too large to upload. Please trim or compress it and try again.', 31 31 ); 32 32 }); 33 + 34 + test('preserves detailed video processing failure reasons', () { 35 + const error = VideoUploadException( 36 + 'Video processing failed. Unsupported video codec: hev1.', 37 + ); 38 + 39 + final message = ErrorMessages.getOperationErrorMessage('post', error); 40 + 41 + expect( 42 + message, 43 + 'Video processing failed. Unsupported video codec: hev1.', 44 + ); 45 + }); 46 + 47 + test( 48 + 'preserves detailed upload failure reasons from generic exceptions', 49 + () { 50 + final message = ErrorMessages.getOperationErrorMessage( 51 + 'post', 52 + Exception('Failed to upload video. Unsupported container: mov.'), 53 + ); 54 + 55 + expect(message, 'Failed to upload video. Unsupported container: mov.'); 56 + }, 57 + ); 33 58 }); 34 59 }