[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: avoid no-op video editor re-exports

+127 -6
+127 -6
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); 50 51 51 52 final _editorKey = GlobalKey<ProImageEditorState>(); 52 53 final bool _useMaterialDesign = ··· 500 501 Future<void> generateVideo(CompleteParameters parameters) async { 501 502 unawaited(_videoController.pause()); 502 503 unawaited(_audioService.pause()); 503 - final directory = await getTemporaryDirectory(); 504 504 505 505 final customAudioTrack = parameters.customAudioTrack; 506 506 _selectedSoundRef = _decodeStrongRef(customAudioTrack?.id); 507 + 508 + if (_canUseOriginalVideo(parameters)) { 509 + _outputPath = await _video.safeFilePath(); 510 + return; 511 + } 512 + 513 + final directory = await getTemporaryDirectory(); 507 514 508 515 double overlayVolume = 0; 509 516 double originalVolume = 1; ··· 522 529 customAudioTrack, 523 530 ); 524 531 532 + final transform = _buildExportTransform(parameters); 525 533 final exportModel = VideoRenderData( 526 534 id: _taskId, 527 535 videoSegments: [VideoSegment(video: _video, volume: originalVolume)], ··· 536 544 .toList(), 537 545 startTime: parameters.startTime, 538 546 endTime: parameters.endTime, 539 - transform: _buildExportTransform(parameters), 540 - bitrate: _videoMetadata.bitrate, 547 + transform: transform, 548 + bitrate: _targetExportBitrate(transform), 541 549 audioTracks: customAudioPath != null 542 550 ? [ 543 551 VideoAudioTrack( ··· 556 564 ); 557 565 } 558 566 567 + bool _canUseOriginalVideo(CompleteParameters parameters) { 568 + if (parameters.layers.isNotEmpty || 569 + parameters.colorFilters.isNotEmpty || 570 + parameters.customAudioTrack != null || 571 + (parameters.blur).abs() > 0.001 || 572 + _hasTrim(parameters) || 573 + _hasRotation(parameters) || 574 + parameters.flipX || 575 + parameters.flipY || 576 + (_proVideoController?.isAudioEnabled == false)) { 577 + return false; 578 + } 579 + 580 + if (!widget.storyMode) { 581 + return !parameters.isTransformed; 582 + } 583 + 584 + return _isStoryExportTransformIdentity(); 585 + } 586 + 587 + bool _hasTrim(CompleteParameters parameters) { 588 + final startTime = parameters.startTime ?? Duration.zero; 589 + final endTime = parameters.endTime ?? _videoMetadata.duration; 590 + 591 + return startTime > _trimTolerance || 592 + _videoMetadata.duration - endTime > _trimTolerance; 593 + } 594 + 595 + bool _hasRotation(CompleteParameters parameters) { 596 + final normalizedTurns = parameters.rotateTurns % 4; 597 + return normalizedTurns != 0; 598 + } 599 + 600 + bool _isStoryExportTransformIdentity() { 601 + final coverCrop = _computeStoryCoverCrop(_videoMetadata.resolution); 602 + final sourceWidth = _videoMetadata.resolution.width.round(); 603 + final sourceHeight = _videoMetadata.resolution.height.round(); 604 + 605 + return coverCrop.x == 0 && 606 + coverCrop.y == 0 && 607 + coverCrop.width == sourceWidth && 608 + coverCrop.height == sourceHeight && 609 + _storyTargetWidth(coverCrop) == coverCrop.width && 610 + _storyTargetHeight(coverCrop) == coverCrop.height; 611 + } 612 + 613 + int? _targetExportBitrate(ExportTransform? transform) { 614 + final sourceBitrate = _videoMetadata.bitrate; 615 + if (sourceBitrate <= 0) { 616 + return null; 617 + } 618 + 619 + final resolution = _targetExportResolution(transform); 620 + final longEdge = math.max(resolution.width, resolution.height); 621 + final bitrateCeiling = switch (longEdge) { 622 + <= 960 => 3000000, 623 + <= 1280 => 5000000, 624 + <= 2560 => 8000000, 625 + _ => 35000000, 626 + }; 627 + 628 + return math.min(sourceBitrate, bitrateCeiling); 629 + } 630 + 631 + Size _targetExportResolution(ExportTransform? transform) { 632 + if (transform == null) { 633 + return _videoMetadata.resolution; 634 + } 635 + 636 + final width = (transform.width ?? _videoMetadata.resolution.width) 637 + .toDouble(); 638 + final height = (transform.height ?? _videoMetadata.resolution.height) 639 + .toDouble(); 640 + 641 + return Size( 642 + width * (transform.scaleX ?? 1), 643 + height * (transform.scaleY ?? 1), 644 + ); 645 + } 646 + 559 647 ExportTransform? _buildExportTransform(CompleteParameters parameters) { 560 648 if (!widget.storyMode) { 561 649 if (!parameters.isTransformed) { ··· 574 662 } 575 663 576 664 final coverCrop = _computeStoryCoverCrop(_videoMetadata.resolution); 577 - final targetWidth = _storyCanvasSize.width.round(); 578 - final targetHeight = _storyCanvasSize.height.round(); 665 + final targetWidth = _storyTargetWidth(coverCrop); 666 + final targetHeight = _storyTargetHeight(coverCrop); 579 667 580 668 return ExportTransform( 581 669 width: coverCrop.width, ··· 590 678 ); 591 679 } 592 680 681 + int _storyTargetWidth(_StoryCoverCrop coverCrop) { 682 + return _evenDimension( 683 + math.min(_storyCanvasSize.width.round(), coverCrop.width), 684 + ); 685 + } 686 + 687 + int _storyTargetHeight(_StoryCoverCrop coverCrop) { 688 + return _evenDimension( 689 + math.min(_storyCanvasSize.height.round(), coverCrop.height), 690 + ); 691 + } 692 + 693 + int _evenDimension(int value) { 694 + if (value <= 2) { 695 + return 2; 696 + } 697 + return value.isEven ? value : value - 1; 698 + } 699 + 593 700 _StoryCoverCrop _computeStoryCoverCrop(Size sourceSize) { 594 701 final sourceWidth = math.max(1, sourceSize.width.round()); 595 702 final sourceHeight = math.max(1, sourceSize.height.round()); ··· 638 745 Navigator.pop( 639 746 context, 640 747 VideoEditorResult( 641 - video: XFile(_outputPath!, mimeType: 'video/mp4'), 748 + video: XFile(_outputPath!, mimeType: _videoMimeType(_outputPath!)), 642 749 soundRef: _selectedSoundRef, 643 750 embeds: pendingStoryEmbeds, 644 751 ), ··· 650 757 clearPendingStoryEmbeds(); 651 758 Navigator.pop(context); 652 759 } 760 + } 761 + 762 + String _videoMimeType(String videoPath) { 763 + final lowerPath = videoPath.toLowerCase(); 764 + if (lowerPath.endsWith('.mov')) { 765 + return 'video/quicktime'; 766 + } 767 + if (lowerPath.endsWith('.avi')) { 768 + return 'video/x-msvideo'; 769 + } 770 + if (lowerPath.endsWith('.webm')) { 771 + return 'video/webm'; 772 + } 773 + return 'video/mp4'; 653 774 } 654 775 655 776 @override