yt_extractor 0.1.0
yt_extractor: ^0.1.0 copied to clipboard
A Flutter plugin that wraps the NewPipe Extractor. Extract YouTube (and other service) streams, audio, video, metadata, search, comments, channels, playlists and trending content — without any API keys.
yt_extractor #
A Flutter plugin that wraps the NewPipe Extractor
(Android) and exposes it over a MethodChannel. Extract streams, audio,
video, metadata, search results, comments, channels, playlists and trending
content from YouTube — and other services — without any API keys.
Everything runs on a background worker pool, so calling the extractor never blocks the UI thread.
Features #
| Feature | Method |
|---|---|
| Initialize the extractor + downloader | init |
| Resolve which service handles a URL | getServiceByUrl |
| List all supported services | getServices |
| Full stream metadata + all streams | getStreamInfo |
| Related streams for a video | getRelatedStreams |
| Paginated search (with content filters) | search |
| Autocomplete suggestions | getSuggestions |
| Paginated comments | getComments |
| Channel info + its tabs and first page | getChannel |
| List a channel's tabs | getChannelTabs |
| Paginated tab content | getChannelTab |
| Playlist info + first page | getPlaylist |
| List kiosks (Trending, Music, …) | getKiosks |
| Paginated kiosk content | getKiosk |
| Fetch the next page of any session | loadMore / loadMoreComments |
| Free a native session early | dispose |
Requirements #
- Flutter 3.24+, Dart 3.12+
- Android only (for now): minSdk 24, the
INTERNETpermission, core library desugaring, and the JitPack repository.
App setup #
Add the JitPack repository to your Android Gradle files, then the plugin
dependency to your Dart pubspec.yaml:
dependencies:
yt_extractor: ^0.1.0
Your android/app/build.gradle.kts needs JitPack and desugaring:
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.1.5")
}
// android/build.gradle.kts
allprojects {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
Getting started #
Create a single YtExtractor instance and reuse it. It is cheap and
stateless on the Dart side; native sessions are created per call.
import 'package:yt_extractor/yt_extractor.dart';
final extractor = YtExtractor();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await extractor.init();
final service = await extractor.getServiceByUrl(
'https://youtube.com/watch?v=dQw4w9WgXcQ',
);
print('Resolved to service: ${service.name} (id ${service.id})');
}
init() is idempotent — safe to call more than once, and optional: the
first extraction call initializes the native side automatically if you
haven't. Call it early only if you want initialization on your own schedule
(e.g. at app startup).
Extracting a stream #
final info = await extractor.getStreamInfo(
'https://youtube.com/watch?v=dQw4w9WgXcQ',
);
print(info.name); // title
print(info.uploaderName); // channel name
print(info.duration); // seconds
print(info.viewCount); // views
info.thumbnailUrl; // convenience: first thumbnail URL
// Streams: pick the best audio (highest bitrate) and highest video.
final audio = info.bestAudioStream;
final video = info.bestVideoStream;
print('${audio?.averageBitrate} kbps -> ${audio?.url}');
print('${video?.resolution} @ ${video?.fps}fps -> ${video?.url}');
// Chapters/segments (e.g. song sections), when the extractor provides them.
print(info.segments.map((s) => s.title));
// Related videos. Two ways:
// 1. Opt into the related rail on getStreamInfo (one extra network call):
final infoWithRelated = await extractor.getStreamInfo(url, includeRelated: true);
print(infoWithRelated.relatedStreams.length);
// 2. Or fetch them separately, leaving the StreamInfo payload lean:
final related = await extractor.getRelatedStreams(url);
Signed URLs expire (~6h). Don't persist them; re-extract
getStreamInfoto get fresh URLs. Note the naming collision: on aStreamInfo, the best audio isinfo.bestAudioStream; on theAudioStreamitself, its ownbestAudioStreamfield holds the URL for the remote media source (e.g. HLS).
Playing audio #
Pass the audio stream URL to any audio player, e.g. just_audio:
final player = AudioPlayer();
await player.setUrl(info.bestAudioStream!.url);
player.play();
Searching #
final results = await extractor.search(
'lofi hip hop',
filter: SearchFilter.musicSongs,
);
for (final item in results.items) {
print('${item.name} — ${item.uploaderName}');
}
// Autocomplete suggestions for a query.
final suggestions = await extractor.getSuggestions('lofi');
Available filters: SearchFilter.all, videos, channels, playlists,
musicSongs, musicVideos, musicAlbums, musicPlaylists,
musicArtists.
Comments #
final page = await extractor.getComments(videoUrl);
for (final comment in page.items) {
print('${comment.author}: ${comment.commentText}');
print('pinned=${comment.isPinned} '
'hearted=${comment.isHeartedByUploader} '
'edited=${comment.isEdited}');
}
Every Comment carries flag fields the extractor provides:
isPinned, isHeartedByUploader, isEdited, isChannelOwner,
isCreatorReply, isUploaderVerified, plus replyCount.
Channels & playlists #
// Channel header + first page of content + its tabs.
final channel = await extractor.getChannel(channelUrl);
print(channel.name);
print('${channel.subscriberCount} subscribers');
for (final tab in channel.tabs) {
print('tab: ${tab.id}');
}
channel.avatarUrl; // convenience: first avatar URL
channel.bannerUrl;
// A specific tab's content (Videos, Shorts, Live, Playlists, …).
final videos = await extractor.getChannelTab(channel.tabs.first.url);
// Playlist header + first page of streams.
final playlist = await extractor.getPlaylist(playlistUrl);
print('${playlist.name} by ${playlist.uploaderName}');
// streamCount is -1 when the extractor can't determine it up front —
// paginate with loadMore to discover the real length.
playlist.thumbnailUrl;
For images, use the full lists: channel.avatars, channel.banners,
playlist.thumbnails, info.thumbnails. Each YtImage has url,
width, height and resolutionLevel; every model also exposes a
xxxUrl convenience getter for the first entry.
Services & kiosks #
final services = await extractor.getServices();
for (final s in services) {
print('${s.id}: ${s.name}');
}
final kiosks = await extractor.getKiosks(); // defaults to YouTube
for (final k in kiosks) {
print('${k.id} — ${k.name}'); // e.g. "trending — Trending"
}
final trending = await extractor.getKiosk('trending');
Pagination #
Every paged call (search, getChannel, getChannelTab, getPlaylist,
getKiosk, getComments) returns a PagedResults<T>:
final page = await extractor.search('lofi');
if (page.hasNext) {
final next = await extractor.loadMore(page.sessionId);
// Keep calling loadMore with the *latest* sessionId to page further.
}
// Comments page through loadMoreComments instead.
final nextComments = await extractor.loadMoreComments(commentsPage.sessionId);
// Release the native session early (also evicted on expiry).
await extractor.dispose(page.sessionId);
PagedResults exposes sessionId, items and hasNext. Pass the session
from the most recent page each time.
Errors #
All failures throw a typed subclass of YtExtractorException:
| Exception | Meaning |
|---|---|
ReCaptchaException |
YouTube challenged the request (usually HTTP 429). Wait and retry later. |
NetworkException |
DNS, connection refused, timeouts. |
SessionExpiredException |
Pagination session is missing/expired. |
InvalidArgumentException |
The call was made with bad arguments. |
NotFoundException |
The resource could not be found. |
ExtractionException |
The extractor failed to parse the page. |
ExtractorTimeoutException |
The call exceeded the configured timeout. |
PluginNotRegisteredException |
init() hasn't completed on this platform. |
try {
final info = await extractor.getStreamInfo(url);
} on ReCaptchaException catch (e) {
// Back off and retry later.
} on NetworkException catch (e) {
// Offline?
}
The default per-call timeout is 90s; configure it on the constructor:
final extractor = YtExtractor(timeout: const Duration(minutes: 2));
Example app #
The bundled example/ is a complete demo: search with filters and
suggestions, kiosk browsing, stream pages with just_audio playback and
external video open, channel and playlist pages, and infinite scrolling.
cd example
flutter run
FAQ #
Do I need an API key? No. The extractor scrapes the public web endpoints directly, exactly like the NewPipe app.
Why do stream URLs stop working after a few hours? Signed media URLs
expire. Re-run getStreamInfo to refresh them.
Is this a video player? No — it extracts metadata and stream URLs. Pair
it with just_audio / media_kit / video_player for playback.
Does it support YouTube Music? Yes for public content — music search
filters (SearchFilter.music*) and kiosks hit the same endpoints.
Why is it Android-only? The NewPipe Extractor is a JVM library. Other platforms would need a different transport (e.g. a Dart reimplementation).
Can it bypass age gates / logged-in content? No. It only handles public
content; there is no authentication and no po_token handling, so some
videos may refuse to extract.
Limitations #
- Signed URLs expire; re-extract rather than caching forever.
- No authentication, no age-gate bypass, no
po_tokensupport. - Rate limiting / captchas (
ReCaptchaException) can happen; retry later. - Android only.
Contributing #
See CONTRIBUTING.md. Run flutter analyze and
flutter test before opening a PR.
License #
MIT — see LICENSE.