Skip to content

Commit

Permalink
Improve HLS support + Update stream describe() + New example
Browse files Browse the repository at this point in the history
  • Loading branch information
Hexer10 committed Oct 18, 2024
1 parent 236a67c commit 8f678fc
Show file tree
Hide file tree
Showing 11 changed files with 139 additions and 77 deletions.
19 changes: 17 additions & 2 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
---
name: Bug report
about: Create a report to help us improve
about: Create a bug report, make sure to follow the template otherwise the issue might be closed.
title: "[BUG]"
labels: bug
assignees: ''

---



**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Include the code which doesn't work in the code markdown..
Include the code which doesn't work in the code markdown...
```dart
```

**Stacktrace**
Include here the stacktrace (if applicable).
Include also the full logs by adding the following lines to the start of your code:
```dart
Logger.root.level = Level.FINER;
Logger.root.onRecord.listen((e) {
print(e);
if (e.error != null) {
print(e.error);
print(e.stackTrace);
}
});
```
If too long please use a service like https://gist.github.com/ or https://pastebin.com/

**Enviroment: (please complete the following information):**
- Enviroment: [Flutter o Dart VM]
- Version [e.g. 2.8.4]
- YoutubeExplode Version [e.g. ^2.5.0]

**Additional context**
Add any other context about the problem here.
54 changes: 26 additions & 28 deletions example/example.dart
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
// ignore_for_file: avoid_print
import 'package:logging/logging.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';

Future<void> main() async {
Logger.root.level = Level.FINER;
Logger.root.onRecord.listen((record) {
print(
'[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}');
if (record.error != null) {
print(record.error);
print(record.stackTrace);
}
});

final yt = YoutubeExplode();
final streamInfo =
await yt.videos.streams.getManifest('qCnQEsljFt0', ytClients: [
YoutubeApiClient.ios,
YoutubeApiClient.android,
YoutubeApiClient.tv,
YoutubeApiClient.androidVr,
YoutubeApiClient.mediaConnect,
YoutubeApiClient.tvSimplyEmbedded
]);

print(streamInfo);
final my = streamInfo.hls.last;
// final file = File('local/vid.ts').openWrite();
// await yt.videos.streams.get(my).pipe(file);
// await file.flush();
// await file.close();

// Get the video metadata.
final video = await yt.videos.get('fRh_vgS2dFE');
print(video.title); // ^ You can pass both video URLs or video IDs.

final manifest = await yt.videos.streams.getManifest('fRh_vgS2dFE',
// You can also pass a list of preferred clients, otherwise the library will handle it:
ytClients: [
YoutubeApiClient.ios,
YoutubeApiClient.androidVr,
]);

// Print all the available streams.
print(manifest);

// Get the audio streams.
final audio = manifest.audioOnly;

// Download it
final stream = yt.videos.streams.get(audio.first);
// then pipe the stream to a file...

// Or you can use the url to stream it directly.
audio.first.url; // This is the audio stream url.

// Make sure to handle the file extension properly. Especially m3u8 streams might require further processing.

// Close the YoutubeExplode's http client.
yt.close();
Expand Down
50 changes: 30 additions & 20 deletions lib/src/reverse_engineering/hls_manifest.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,25 @@ class HlsManifest {
List<_StreamInfo> get streams {
final streams = <_StreamInfo>[];
for (final video in videos) {
final type = video.params['TYPE'];
if (type != null && type != 'AUDIO') {
// TODO: type 'SUBTITLES' not supported.
continue;
}
// The tag is the number after the /itag/ segment in the url
final videoParts = video.url.split('/');
final itag = int.parse(videoParts[videoParts.indexOf('itag') + 1]);

var bandwidth = int.tryParse(video.params['BANDWIDTH'] ?? '');
final codecs = video.params['CODECS']?.replaceAll('"', '').split(',');
final audioCodec = codecs?.first;
final videoCodec = codecs?.last;
final resolution = video.params['RESOLUTION']?.split('x');
final videoWidth = int.tryParse(resolution?[0] ?? '');
final videoHeight = int.tryParse(resolution?[1] ?? '');
final framerate = int.tryParse(video.params['FRAME-RATE'] ?? '');
final audioItag = int.tryParse(video.params['AUDIO']?.trimQuotes() ?? '');

// To find the file size look for the segments after the sgoap and sgovp parameters (audio + video)
// Then URL decode the value and find the clen= parameter
String? sgoap;
Expand All @@ -68,21 +84,17 @@ class HlsManifest {
if (sgoap != null) {
audioClen =
int.parse(RegExp(r'clen=(\d+)').firstMatch(sgoap)!.group(1)!);
if (bandwidth == null) {
final dur = double.parse(
RegExp(r'dur=(\d+\.\d+)').firstMatch(sgoap)!.group(1)!);
bandwidth = (audioClen / dur).round() * 8;
}
}
if (sgovp != null) {
videoClen =
int.parse(RegExp(r'clen=(\d+)').firstMatch(sgovp)!.group(1)!);
}

final bandwidth = int.tryParse(video.params['BANDWIDTH'] ?? '');
final codecs = video.params['CODECS']?.replaceAll('"', '').split(',');
final audioCodec = codecs?.first;
final videoCodec = codecs?.last;
final resolution = video.params['RESOLUTION']?.split('x');
final videoWidth = int.tryParse(resolution?[0] ?? '');
final videoHeight = int.tryParse(resolution?[1] ?? '');
final framerate = int.tryParse(video.params['FRAME-RATE'] ?? '');

streams.add(
_StreamInfo(
itag,
Expand All @@ -97,6 +109,7 @@ class HlsManifest {
bandwidth ?? 0,
videoClen == null,
audioClen == null,
audioItag,
),
);
}
Expand All @@ -123,17 +136,6 @@ class HlsManifest {
}
}

/*
BANDWIDTH=202508,CODECS="mp4a.40.5,avc1.4d400c",RESOLUTION=256x144,FRAME-RATE=30,VIDEO-RANGE=SDR,CLOSED-CAPTIONS=NONE
The hls stream provide the following information:
- BANDWIDTH: bytes
- CODECS: comma separated list of codecs
- RESOLUTION: width x height
- FRAME-RATE: frames per second
from the url:
- itag
*/
class _StreamInfo extends StreamInfoProvider {
@override
final int tag;
Expand Down Expand Up @@ -177,6 +179,9 @@ class _StreamInfo extends StreamInfoProvider {
@override
final bool videoOnly;

@override
final int? audioItag;

_StreamInfo(
this.tag,
this.url,
Expand All @@ -190,6 +195,7 @@ class _StreamInfo extends StreamInfoProvider {
this.bitrate,
this.audioOnly,
this.videoOnly,
this.audioItag,
) : codec = MediaType('application', 'vnd.apple.mpegurl', {
'codecs': [
if (audioCodec != null) audioCodec,
Expand All @@ -200,3 +206,7 @@ class _StreamInfo extends StreamInfoProvider {
@override
StreamSource get source => StreamSource.hls;
}

extension on String {
String trimQuotes() => substring(1, length - 1);
}
2 changes: 2 additions & 0 deletions lib/src/reverse_engineering/models/stream_info_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,6 @@ abstract class StreamInfoProvider {
bool get audioOnly => false;

bool get videoOnly => false;

int? get audioItag => null;
}
1 change: 1 addition & 0 deletions lib/src/videos/streams/mixins/hls_stream_info.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import '../../../../youtube_explode_dart.dart';

mixin HlsStreamInfo on StreamInfo {
/// The tag of the audio stream related to this stream.
int? get audioItag => null;
}
49 changes: 35 additions & 14 deletions lib/src/videos/streams/mixins/stream_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,49 +65,57 @@ extension StreamInfoIterableExt<T extends StreamInfo> on Iterable<T> {

/// Print a formatted text of all the streams. Like youtube-dl -F option.
String describe() {
final column = _Column(['format code', 'extension', 'resolution', 'note']);
final column = _Column([
'format code',
'extension',
'resolution',
'quality',
'bitrate',
'size',
'codecs',
'info'
]);
// Sort the streams:
// - First audio only streams.
// - Then sort by resolution.
// - Then sort by extension.
// - Then sort by bitrate.
final sorted = toList()
..sort((a, b) {
final aIsOnlyAudio =
(a is AudioOnlyStreamInfo) || (a is HlsAudioStreamInfo);
final bIsOnlyAudio =
(b is AudioOnlyStreamInfo) || (b is HlsAudioStreamInfo);

if (aIsOnlyAudio && !bIsOnlyAudio) {
return -1;
} else if (!aIsOnlyAudio && bIsOnlyAudio) {
return 1;
}
if (a is AudioStreamInfo && b is! AudioStreamInfo) {
return -1;
} else if (a is! AudioStreamInfo && b is AudioStreamInfo) {
return 1;
}

if (a is VideoStreamInfo && b is VideoStreamInfo) {
final resolution = a.videoResolution.compareTo(b.videoResolution);
if (resolution != 0) {
return resolution;
}
}
return a.container.name.compareTo(b.container.name);
return a.bitrate.compareTo(b.bitrate);
});

for (final e in sorted) {
column.write([
e.tag,
e.container.name,
if (e is VideoStreamInfo) e.videoResolution else 'audio only',
e.qualityLabel,
if (e is VideoStreamInfo)
'${e.qualityLabel}${e.framerate.framesPerSecond}'
else
e.qualityLabel,
e.bitrate,
e.size,
e.codec.parameters['codecs'],
if (e is VideoStreamInfo) e.framerate,
if (e is VideoOnlyStreamInfo || e is HlsVideoStreamInfo) 'video only',
// if (e is AudioOnlyStreamInfo) 'audio only',
if (e is MuxedStreamInfo || e is HlsMuxedStreamInfo) 'muxed',
e.size,
if (e case AudioStreamInfo(:AudioTrack audioTrack))
audioTrack.displayName,
]);
Expand All @@ -130,9 +138,19 @@ class _Column {
String toString() {
final headerLen = <int>[];
final buffer = StringBuffer();
for (final e in header) {
headerLen.add(e.length + 2);
buffer.write('$e ');

// Find the longest string for each column.
final longest = _values.map((e) {
return e
.reduce((value, element) =>
value.length > element.length ? value : element)
.length;
}).toList();

for (final (i, e) in header.indexed) {
final pad = longest[i] > (e.length + 2) ? longest[i] - e.length + 2 : 2;
headerLen.add(e.length + pad);
buffer.write(e.padRight(e.length + pad));
}
buffer.writeln();

Expand All @@ -143,6 +161,9 @@ class _Column {
buffer.write(', $v');
continue;
}
if (v.length > headerLen[i]) {
headerLen[i] = v.length + 2;
}
buffer.write(v.padRight(headerLen[i]));
}
buffer.writeln();
Expand Down
18 changes: 11 additions & 7 deletions lib/src/videos/streams/stream_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,23 @@ class StreamClient {
///
/// See [YoutubeApiClient] for all the possible clients that can be set using the [ytClients] parameter.
/// If [ytClients] is null the library automatically manages the clients, otherwise only the clients provided are used.
/// Currently by default the [YoutubeApiClient.android] and [YoutubeApiClient.ios] clients are used, if the extraction fails the [YoutubeApiClient.tvSimplyEmbedded] client is used instead.
/// Currently by default the [YoutubeApiClient.tv] and [YoutubeApiClient.ios] clients are used, if the extraction fails the [YoutubeApiClient.tvSimplyEmbedded] client is used instead.
///
/// If [requireWatchPage] (default: true) is set to false the watch page is not used to extract the streams (so the process can be faster) but
/// it COULD be less reliable (not tested thoroughly).
/// If the extracted streams require signature decoding for which the watch page is required, the client will automatically fetch the watch page anyways (e.g. [YoutubeApiClient.tvSimplyEmbedded]).
///
/// If the extraction fails an exception is thrown, to diagnose the issue enable the logging from the `logging` package, and open an issue with the output.
/// For example:
/// For example add at the beginning of your code:
/// ```dart
/// Logger.root.level = Level.FINER;
/// Logger.root.onRecord.listen(print);
/// // run yt related code ...
///
/// Logger.root.onRecord.listen((e) {
/// print(e);
/// if (e.error != null) {
/// print(e.error);
/// print(e.stackTrace);
/// }
/// });
/// ```
Future<StreamManifest> getManifest(dynamic videoId,
{@Deprecated(
Expand All @@ -51,7 +55,7 @@ class StreamClient {
bool requireWatchPage = true}) async {
videoId = VideoId.fromString(videoId);
final clients =
ytClients ?? [YoutubeApiClient.ios, YoutubeApiClient.android];
ytClients ?? [YoutubeApiClient.ios, YoutubeApiClient.tv];

final uniqueStreams = LinkedHashSet<StreamInfo>(
equals: (a, b) {
Expand Down Expand Up @@ -292,7 +296,7 @@ class StreamClient {
videoResolution,
framerate,
stream.codec,
0,
stream.audioItag,
);
} else {
yield HlsMuxedStreamInfo(
Expand Down
6 changes: 5 additions & 1 deletion lib/src/videos/streams/types/hls/hls_audio_stream_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,19 @@ class HlsAudioStreamInfo with StreamInfo, AudioStreamInfo, HlsStreamInfo {
final StreamContainer container;

@override

/// For HLS streams this is an approximation.
final FileSize size;

@override

/// For HLS streams this is an approximation.
final Bitrate bitrate;

@override
final String audioCodec;

/// This is always empty for hls streams
/// Always empty.
@override
List<Fragment> get fragments => const [];

Expand Down
Loading

0 comments on commit 8f678fc

Please sign in to comment.