Hexer10 / youtube_explode_dart

Dart library to interact with many Youtube APIs
https://pub.dev/packages/youtube_explode_dart
BSD 3-Clause "New" or "Revised" License
325 stars 143 forks source link

[BUG] Access to rr3---sn-uxaxjvhxbt2u-2nql.googlevideo.com was denied #305

Closed AhmedAlaaGenina closed 1 week ago

AhmedAlaaGenina commented 4 weeks ago

Describe the bug I get encrypted link and not work when I try to åçopen it in browser or when i try to open it in player using media kit package in flutter

To Reproduce

My full Code

import 'dart:async';
import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart' as yt;

/// YPlayerController
class YPlayerController extends ChangeNotifier {
  final String youtubeUrl;
  final bool autoPlay;
  final bool allowFullScreen;
  final double? aspectRatio;
  final bool allowMuting;
  final Widget? errorWidget;
  final Widget? loadingWidget;
  final Widget? placeholder;
  final Widget? overlay;

  Player? _player;
  VideoController? _videoController;
  final yt.YoutubeExplode _yt = yt.YoutubeExplode();
  List<VideoQuality> _qualities = [];
  VideoQuality? _currentQuality;
  YPlayerState _state = YPlayerState(
    status: YPlayerStatus.initial,
    position: Duration.zero,
    duration: Duration.zero,
    isFullScreen: false,
    isMuted: false,
    playerActionLoading: false,
    volume: 100.0,
    playbackSpeed: 1.0,
  );

  final StreamController<YPlayerState> _stateController =
      StreamController<YPlayerState>.broadcast();

  Stream<YPlayerState> get onStateChanged => _stateController.stream;
  List<VideoQuality> get qualities => _qualities;
  VideoQuality? get currentQuality => _currentQuality;
  Timer? _positionUpdateTimer;

  YPlayerController({
    required this.youtubeUrl,
    this.autoPlay = true,
    this.allowFullScreen = true,
    this.allowMuting = true,
    this.errorWidget,
    this.loadingWidget,
    this.placeholder,
    this.overlay,
    this.aspectRatio,
  });

  YPlayerState get currentState => _state;

  Future<void> initialize(BuildContext context) async {
    _qualities = [];
    _updateState(status: YPlayerStatus.loading);
    try {
      final video = await _yt.videos.get(youtubeUrl);
      final manifest = await _yt.videos.streamsClient.getManifest(
        video.id,
        ytClients: [yt.YoutubeApiClient.safari, yt.YoutubeApiClient.androidVr],
      );
      final muxedStreams = manifest.muxed;
      // final muxedUrls = muxedStreams.toList().toSet();
      final videoUrls = manifest.videoOnly;
      final audioUrls = manifest.audioOnly;
      _qualities = videoUrls
          .map(
            (s) => VideoQuality(s.qualityLabel, s.url),
          )
          .toList();
      for (var quality in _qualities) {
        log('Video Quality: ${quality.label}, URL: ${quality.url}');
      }
      // final bestVideo = videoUrls.withHighestBitrate();
      // final bestAudio = audioUrls.withHighestBitrate();
      final bestVideo = videoUrls.last;
      final bestAudio = audioUrls.first;
      final highestQualityStream = muxedStreams.withHighestBitrate();
      _qualities.add(VideoQuality("Auto", highestQualityStream.url));
      _currentQuality = _qualities.last;
      _player = Player();
      _videoController = VideoController(_player!);
      log('highestQualityStream : ${highestQualityStream.url}');
      log('videoHighestQualityStream : ${bestVideo.url}');
      log('audioHighestQualityStream : ${bestAudio.url}');
      await _player!
          .open(Media(highestQualityStream.url.toString()), play: autoPlay);
      // await _player!.open(Media(bestVideo.url.toString()), play: autoPlay);
      // await _player!.setAudioTrack(AudioTrack.uri(bestAudio.url.toString()));

      _setupListeners();
      _updateState(
        status: YPlayerStatus.playing,
        duration: await _player!.stream.duration.first,
      );
    } catch (e) {
      _updateState(
        status: YPlayerStatus.error,
        errorMessage: e.toString(),
      );
    }
  }

  void _setupListeners() {
    _player?.stream.position.listen((position) {
      _updateState(position: position);
    });

    _player?.stream.duration.listen((duration) {
      _updateState(duration: duration);
    });

    _player?.stream.playing.listen((isPlaying) {
      _updateState(
        status: isPlaying ? YPlayerStatus.playing : YPlayerStatus.paused,
      );
    });

    _player?.stream.completed.listen((completed) {
      if (completed) {
        _updateState(status: YPlayerStatus.ended);
      }
    });

    _player?.stream.volume.listen((volume) {
      _updateState(
        volume: volume.toDouble(),
        isMuted: volume == 0,
      );
    });

    _player?.stream.rate.listen((rate) {
      _updateState(playbackSpeed: rate);
    });
  }

  void _updateState({
    YPlayerStatus? status,
    Duration? position,
    Duration? duration,
    bool? isFullScreen,
    bool? isMuted,
    double? volume,
    bool? playerActionLoading,
    double? playbackSpeed,
    String? errorMessage,
  }) {
    _state = _state.copyWith(
      status: status,
      position: position,
      duration: duration,
      isFullScreen: isFullScreen,
      isMuted: isMuted,
      volume: volume,
      playerActionLoading: playerActionLoading,
      playbackSpeed: playbackSpeed,
      errorMessage: errorMessage,
    );
    _stateController.add(_state);
    notifyListeners();
  }

  Future<void> changeQuality(VideoQuality quality) async {
    _updateState(status: YPlayerStatus.loading);
    try {
      final currentPosition = _player?.state.position ?? Duration.zero;
      log('current position : $currentPosition');
      // _player?.dispose();
      // _player = Player();
      // _videoController = VideoController(_player!);
      await _player?.open(Media(quality.url.toString()), play: false);
      await seekTo(currentPosition);
      _currentQuality = quality;
      _updateState(status: YPlayerStatus.playing);
    } catch (e) {
      _updateState(
        status: YPlayerStatus.error,
        errorMessage: e.toString(),
      );
    }
    notifyListeners();
  }

  // Playback controls
  Future<void> play() async {
    await _player?.play();
  }

  Future<void> pause() async {
    await _player?.pause();
  }

  void togglePlay() {
    _player?.playOrPause();
  }

  Future<void> seekTo(Duration position) async {
    _updateState(playerActionLoading: true);
    await pause();
    await _player?.seek(position);
    await play();
    _updateState(playerActionLoading: false);
  }

  Future<void> setVolume(double volume) async {
    await _player?.setVolume(volume);
  }

  Future<void> toggleMute() async {
    final currentVolume = _player?.state.volume ?? 100;
    if (currentVolume == 0) {
      await _player?.setVolume(100);
    } else {
      await _player?.setVolume(0);
    }
  }

  Future<void> setPlaybackSpeed(double speed) async {
    await _player?.setRate(speed);
  }

  void toggleFullScreen() {
    if (_videoController != null) {
      // _videoController!.toggleFullscreen();
      // _updateState(isFullScreen: _videoController!.isFullscreen);
    }
  }

  void disposeController() {
    _positionUpdateTimer?.cancel();
    _player?.dispose();
    _stateController.close();
    _yt.close();
  }

  @override
  void dispose() {
    disposeController();
    super.dispose();
  }
}

/// Custom video controls widget

class CustomVideoControls extends StatefulWidget {
  final YPlayerController controller;
  final VideoState videoState;

  const CustomVideoControls({
    super.key,
    required this.controller,
    required this.videoState,
  });

  @override
  State<CustomVideoControls> createState() => _CustomVideoControlsState();
}

class _CustomVideoControlsState extends State<CustomVideoControls> {
  bool _showControls = true;
  bool _isCompletelyHidden = true;
  Timer? _hideTimer;

  @override
  void initState() {
    super.initState();
    _startHideTimer();
  }

  @override
  void dispose() {
    _hideTimer?.cancel();
    super.dispose();
  }

  void _startHideTimer() {
    _hideTimer?.cancel();
    _hideTimer = Timer(const Duration(seconds: 3), () {
      if (mounted) {
        setState(() => _showControls = false);
        Future.delayed(
          const Duration(milliseconds: 300),
          () => setState(() => _isCompletelyHidden = false),
        );
      }
    });
  }

  void _handleTap() {
    setState(() {
      _showControls = !_showControls;
      _isCompletelyHidden = _showControls;
    });
    if (_showControls) {
      _startHideTimer();
    } else {
      _hideTimer?.cancel();
    }
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior.translucent,
      onTap: _handleTap,
      child: Stack(
        children: [
          // Overlay widget if provided
          // if (widget.controller.overlay != null)
          //   Center(child: widget.controller.overlay!),

          // Controls overlay
          Visibility(
            visible: _isCompletelyHidden,
            child: AnimatedOpacity(
              opacity: _showControls ? 1.0 : 0.0,
              duration: const Duration(milliseconds: 300),
              child: ListenableBuilder(
                listenable: widget.controller,
                builder: (context, _) {
                  final state = widget.controller.currentState;
                  return state.status == YPlayerStatus.playerActionLoading
                      ? const CircularProgressIndicator.adaptive()
                      : Stack(
                          children: [
                            // Top bar
                            Positioned(
                              top: 0,
                              left: 0,
                              right: 0,
                              child: Container(
                                padding: const EdgeInsets.symmetric(
                                    horizontal: 16, vertical: 8),
                                decoration: BoxDecoration(
                                  gradient: LinearGradient(
                                    begin: Alignment.topCenter,
                                    end: Alignment.bottomCenter,
                                    colors: [
                                      Colors.black.withOpacity(0.7),
                                      Colors.transparent,
                                    ],
                                  ),
                                ),
                                child: Row(
                                  mainAxisAlignment:
                                      MainAxisAlignment.spaceBetween,
                                  children: [
                                    // Mute button on the left
                                    _buildCircularButton(
                                      state.isMuted
                                          ? Icons.volume_off
                                          : Icons.volume_up,
                                      onPressed: widget.controller.toggleMute,
                                    ),

                                    // Quality and Speed buttons on the right
                                    Row(
                                      children: [
                                        // Quality button
                                        PopupMenuButton<VideoQuality>(
                                          initialValue:
                                              widget.controller.currentQuality,
                                          onSelected:
                                              widget.controller.changeQuality,
                                          color: Colors.black87,
                                          child: _buildCircularButton(
                                              Icons.settings),
                                          itemBuilder: (context) => widget
                                              .controller.qualities
                                              .map((quality) => PopupMenuItem(
                                                    value: quality,
                                                    child: Text(
                                                      quality.label,
                                                      style: const TextStyle(
                                                          color: Colors.white),
                                                    ),
                                                  ))
                                              .toList(),
                                        ),
                                        const SizedBox(width: 8),
                                        // Speed button
                                        PopupMenuButton<double>(
                                          initialValue: state.playbackSpeed,
                                          onSelected: widget
                                              .controller.setPlaybackSpeed,
                                          color: Colors.black87,
                                          child: Container(
                                            padding: const EdgeInsets.all(8),
                                            decoration: BoxDecoration(
                                              color:
                                                  Colors.black.withOpacity(0.5),
                                              shape: BoxShape.circle,
                                            ),
                                            child: Text(
                                              '${state.playbackSpeed}x',
                                              style: const TextStyle(
                                                  color: Colors.white),
                                            ),
                                          ),
                                          itemBuilder: (context) => [
                                            0.25,
                                            0.5,
                                            0.75,
                                            1.0,
                                            1.25,
                                            1.5,
                                            1.75,
                                            2.0
                                          ]
                                              .map((speed) => PopupMenuItem(
                                                    value: speed,
                                                    child: Text(
                                                      '${speed}x',
                                                      style: const TextStyle(
                                                          color: Colors.white),
                                                    ),
                                                  ))
                                              .toList(),
                                        ),
                                      ],
                                    ),
                                  ],
                                ),
                              ),
                            ),

                            // Center controls
                            Center(
                              child: Row(
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: [
                                  _buildCircularButton(
                                    Icons.replay_10,
                                    onPressed: () => widget.controller.seekTo(
                                        state.position -
                                            const Duration(seconds: 10)),
                                  ),
                                  const SizedBox(width: 32),
                                  state.status ==
                                          YPlayerStatus.playerActionLoading
                                      ? const CircularProgressIndicator
                                          .adaptive()
                                      : _buildCircularButton(
                                          state.status == YPlayerStatus.playing
                                              ? Icons.pause
                                              : Icons.play_arrow,
                                          onPressed:
                                              widget.controller.togglePlay,
                                          size: 64,
                                          iconSize: 32,
                                        ),
                                  const SizedBox(width: 32),
                                  _buildCircularButton(
                                    Icons.forward_10,
                                    onPressed: () => widget.controller.seekTo(
                                        state.position +
                                            const Duration(seconds: 10)),
                                  ),
                                ],
                              ),
                            ),

                            // Bottom controls - only slider and fullscreen
                            Positioned(
                              left: 0,
                              right: 0,
                              bottom: 0,
                              child: Container(
                                padding:
                                    const EdgeInsets.fromLTRB(16, 0, 16, 16),
                                decoration: BoxDecoration(
                                  gradient: LinearGradient(
                                    begin: Alignment.bottomCenter,
                                    end: Alignment.topCenter,
                                    colors: [
                                      Colors.black.withOpacity(0.7),
                                      Colors.transparent,
                                    ],
                                  ),
                                ),
                                child: Row(
                                  children: [
                                    Text(
                                      _formatDuration(state.position),
                                      style:
                                          const TextStyle(color: Colors.white),
                                    ),
                                    Expanded(
                                      child: SliderTheme(
                                        data: const SliderThemeData(
                                          trackHeight: 2,
                                          thumbShape: RoundSliderThumbShape(
                                              enabledThumbRadius: 6),
                                          overlayShape: RoundSliderOverlayShape(
                                              overlayRadius: 12),
                                        ),
                                        child: Slider(
                                          // value: state.position.inMilliseconds.toDouble(),
                                          value: state.position.inMilliseconds
                                              .toDouble()
                                              .clamp(
                                                  0,
                                                  state.duration.inMilliseconds
                                                      .toDouble()),
                                          max: state.duration.inMilliseconds
                                              .toDouble(),

                                          onChanged: (value) =>
                                              widget.controller.seekTo(
                                            Duration(
                                                milliseconds: value.toInt()),
                                          ),
                                        ),
                                      ),
                                    ),
                                    Text(
                                      _formatDuration(state.duration),
                                      style:
                                          const TextStyle(color: Colors.white),
                                    ),
                                    if (widget.controller.allowFullScreen)
                                      IconButton(
                                        icon: Icon(
                                          state.isFullScreen
                                              ? Icons.fullscreen_exit
                                              : Icons.fullscreen,
                                          color: Colors.white,
                                        ),
                                        onPressed:
                                            widget.videoState.toggleFullscreen,
                                      ),
                                  ],
                                ),
                              ),
                            ),
                          ],
                        );
                },
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildCircularButton(
    IconData icon, {
    VoidCallback? onPressed,
    double size = 48,
    double iconSize = 24,
  }) {
    return Container(
      width: size,
      height: size,
      decoration: BoxDecoration(
        color: Colors.black.withOpacity(0.5),
        shape: BoxShape.circle,
      ),
      child: IconButton(
        icon: Icon(icon, color: Colors.white, size: iconSize),
        onPressed: onPressed,
      ),
    );
  }

  String _formatDuration(Duration duration) {
    String twoDigits(int n) => n.toString().padLeft(2, '0');
    final hours = twoDigits(duration.inHours);
    final minutes = twoDigits(duration.inMinutes.remainder(60));
    final seconds = twoDigits(duration.inSeconds.remainder(60));
    return duration.inHours > 0
        ? '$hours:$minutes:$seconds'
        : '$minutes:$seconds';
  }
}

/// YPlayer widget
class YPlayer extends StatefulWidget {
  final YPlayerController controller;

  const YPlayer({
    super.key,
    required this.controller,
  });

  @override
  State<YPlayer> createState() => _YPlayerState();
}

class _YPlayerState extends State<YPlayer> {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) {
        widget.controller.initialize(context);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: widget.controller,
      builder: (context, _) {
        final state = widget.controller.currentState;

        return LayoutBuilder(
          builder: (context, constraints) {
            final aspectRatio = widget.controller.aspectRatio ?? 16 / 9;
            final playerWidth = constraints.maxWidth;
            final playerHeight = playerWidth / aspectRatio;

            Widget playerWidget;
            switch (state.status) {
              case YPlayerStatus.loading:
                playerWidget = widget.controller.loadingWidget ??
                    const Center(child: CircularProgressIndicator.adaptive());
                break;
              case YPlayerStatus.error:
                playerWidget = widget.controller.errorWidget ??
                    Center(
                        child: Text(
                            'Error: ${state.errorMessage ?? "Unknown error"}'));
                break;
              case YPlayerStatus.ready:
              case YPlayerStatus.playing:
              case YPlayerStatus.paused:
              case YPlayerStatus.buffering:
              case YPlayerStatus.ended:
                if (widget.controller._videoController != null) {
                  playerWidget = Video(
                    controller: widget.controller._videoController!,
                    aspectRatio: aspectRatio,
                    width: playerWidth,
                    height: playerHeight,
                    controls: (state) {
                      return CustomVideoControls(
                        controller: widget.controller,
                        videoState: state,
                      );
                    },
                  );
                } else {
                  playerWidget = const SizedBox();
                }
                break;
              default:
                playerWidget =
                    widget.controller.placeholder ?? const SizedBox();
            }

            return SizedBox(
              width: playerWidth,
              height: playerHeight,
              child: playerWidget,
            );
          },
        );
      },
    );
  }
}

/// Video quality class
class VideoQuality {
  VideoQuality(this.label, this.url);

  final String label;
  final Uri url;

  @override
  bool operator ==(covariant VideoQuality other) {
    if (identical(this, other)) return true;

    return other.label == label && other.url == url;
  }

  @override
  int get hashCode => label.hashCode ^ url.hashCode;
}

/// Enhanced status enum for YPlayer
enum YPlayerStatus {
  initial,
  loading,
  ready,
  playing,
  paused,
  buffering,
  ended,
  error,
  playerActionLoading,
}

/// Comprehensive video state class
class YPlayerState {
  final YPlayerStatus status;
  final Duration position;
  final Duration duration;
  final bool isFullScreen;
  final bool isMuted;
  final bool playerActionLoading;
  final double volume;
  final double playbackSpeed;
  final String? errorMessage;

  YPlayerState({
    required this.status,
    required this.position,
    required this.duration,
    required this.isFullScreen,
    required this.isMuted,
    required this.playerActionLoading,
    required this.volume,
    required this.playbackSpeed,
    this.errorMessage,
  });

  YPlayerState copyWith({
    YPlayerStatus? status,
    Duration? position,
    Duration? duration,
    bool? isFullScreen,
    bool? isMuted,
    bool? playerActionLoading,
    double? volume,
    double? playbackSpeed,
    String? errorMessage,
  }) {
    return YPlayerState(
      status: status ?? this.status,
      position: position ?? this.position,
      duration: duration ?? this.duration,
      isFullScreen: isFullScreen ?? this.isFullScreen,
      isMuted: isMuted ?? this.isMuted,
      volume: volume ?? this.volume,
      playerActionLoading: playerActionLoading ?? this.playerActionLoading,
      playbackSpeed: playbackSpeed ?? this.playbackSpeed,
      errorMessage: errorMessage ?? this.errorMessage,
    );
  }
}
Screenshot 2024-10-27 at 2 00 33 PM

Additional context when I try to open it i get black screen in player in flutter and when i get the link and try to open it in browser i get the above image please note when i get link of withHighestBitrate it work fine in browser and player in flutter app

Hexer10 commented 1 week ago

Sorry for the late reply, if this still happens please open a new issue with a minimal reproducible example.