Skip to content
This repository has been archived by the owner on Oct 28, 2023. It is now read-only.

Commit

Permalink
Add support for YouTube Live streams
Browse files Browse the repository at this point in the history
  • Loading branch information
HaarigerHarald committed Jun 25, 2016
1 parent acd498c commit ab01318
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 8 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ Not signature enciphered Videos may work on lower Android versions (untested).
Those videos aren't working:

* Everything private (private videos, bought movies, ...)
* Live streams
* Unavailable in your country
* RTMPE urls (very rare)

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
classpath 'com.android.tools.build:gradle:2.1.2'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,32 @@ public class ExtractorCase extends InstrumentationTestCase {

public void testUsualVideo() throws Throwable {
VideoMeta expMeta = new VideoMeta("YE7VzlLtp-4", "Big Buck Bunny", "Blender Foundation",
"UCSMOQeBJ2RAnuFungnQOxLg", 597, 0);
"UCSMOQeBJ2RAnuFungnQOxLg", 597, 0, false);
extractorTest("http://youtube.com/watch?v=YE7VzlLtp-4", expMeta);
extractorTestDashManifest("http://youtube.com/watch?v=YE7VzlLtp-4");
}


public void testEncipheredVideo() throws Throwable {
VideoMeta expMeta = new VideoMeta("e8X3ACToii0", "Rise Against - Savior", "RiseAgainstVEVO",
"UChMKB2AHNpeuWhalpRYhUaw", 244, 0);
"UChMKB2AHNpeuWhalpRYhUaw", 244, 0, false);
extractorTest("https://www.youtube.com/watch?v=e8X3ACToii0", expMeta);
extractorTestDashManifest("https://www.youtube.com/watch?v=e8X3ACToii0");
}

public void testAgeRestrictVideo() throws Throwable {
VideoMeta expMeta = new VideoMeta("61Ev-YvBw2c", "Test video for age-restriction",
"jpdemoA", "UC95NqtFsDZKlmzOJmZi_g6Q", 14, 0);
"jpdemoA", "UC95NqtFsDZKlmzOJmZi_g6Q", 14, 0, false);
extractorTest("http://www.youtube.com/watch?v=61Ev-YvBw2c", expMeta);
extractorTestDashManifest("http://www.youtube.com/watch?v=61Ev-YvBw2c");
}

public void testLiveStream() throws Throwable {
VideoMeta expMeta = new VideoMeta("njCDZWTI-xg", "NASA Video : Earth From Space Real Footage - Video From The International Space Station ISS",
"Space Videos", "UCakgsb0w7QB0VHdnCc-OVEA", 1800, 0, true);
extractorTest("http://www.youtube.com/watch?v=njCDZWTI-xg", expMeta);
}


private void extractorTestDashManifest(final String youtubeLink)
throws Throwable {
final CountDownLatch signal = new CountDownLatch(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public enum ACodec {
private ACodec aCodec;
private int audioBitrate;
private boolean isDashContainer;
private boolean isHlsContent;

Format(int itag, String ext, int height, VCodec vCodec, ACodec aCodec, boolean isDashContainer) {
this.itag = itag;
Expand All @@ -26,6 +27,7 @@ public enum ACodec {
this.fps = 30;
this.audioBitrate = -1;
this.isDashContainer = isDashContainer;
this.isHlsContent = false;
}

Format(int itag, String ext, VCodec vCodec, ACodec aCodec, int audioBitrate, boolean isDashContainer) {
Expand All @@ -35,6 +37,7 @@ public enum ACodec {
this.fps = 30;
this.audioBitrate = audioBitrate;
this.isDashContainer = isDashContainer;
this.isHlsContent = false;
}

Format(int itag, String ext, int height, VCodec vCodec, ACodec aCodec, int audioBitrate,
Expand All @@ -45,6 +48,18 @@ public enum ACodec {
this.fps = 30;
this.audioBitrate = audioBitrate;
this.isDashContainer = isDashContainer;
this.isHlsContent = false;
}

Format(int itag, String ext, int height, VCodec vCodec, ACodec aCodec, int audioBitrate,
boolean isDashContainer, boolean isHlsContent) {
this.itag = itag;
this.ext = ext;
this.height = height;
this.fps = 30;
this.audioBitrate = audioBitrate;
this.isDashContainer = isDashContainer;
this.isHlsContent = isHlsContent;
}

Format(int itag, String ext, int height, VCodec vCodec, int fps, ACodec aCodec, boolean isDashContainer) {
Expand All @@ -54,6 +69,7 @@ public enum ACodec {
this.audioBitrate = -1;
this.fps = fps;
this.isDashContainer = isDashContainer;
this.isHlsContent = false;
}

/**
Expand Down Expand Up @@ -96,6 +112,10 @@ public VCodec getVideoCodec() {
return vCodec;
}

public boolean isHlsContent() {
return isHlsContent;
}

/**
* The pixel height of the video stream or -1 for audio files.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,20 @@ public class VideoMeta {
private long videoLength;
private long viewCount;

protected VideoMeta(String videoId, String title, String author, String channelId, long videoLength, long viewCount) {
private boolean isLiveStream;

protected VideoMeta(String videoId, String title, String author, String channelId, long videoLength, long viewCount, boolean isLiveStream) {
this.videoId = videoId;
this.title = title;
this.author = author;
this.channelId = channelId;
this.videoLength = videoLength;
this.viewCount = viewCount;
this.isLiveStream = isLiveStream;
}



// 120 x 90
public String getThumbUrl() {
return IMAGE_BASE_URL + videoId + "/default.jpg";
Expand Down Expand Up @@ -63,6 +68,10 @@ public String getChannelId() {
return channelId;
}

public boolean isLiveStream() {
return isLiveStream;
}

/**
* The video length in seconds.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ public abstract class YouTubeExtractor extends AsyncTask<String, Void, SparseArr
private static final Pattern patLength = Pattern.compile("length_seconds=(\\d+?)(&|\\z)");
private static final Pattern patViewCount = Pattern.compile("view_count=(\\d+?)(&|\\z)");

private static final Pattern patHlsvp = Pattern.compile("hlsvp=(.+?)(&|\\z)");
private static final Pattern patHlsItag = Pattern.compile("/itag/(\\d+?)/");

private static final Pattern patItag = Pattern.compile("itag=([0-9]+?)(&|,)");
private static final Pattern patEncSig = Pattern.compile("s=([0-9A-F|\\.]{10,}?)(&|,|\")");
private static final Pattern patUrl = Pattern.compile("url=(.+?)(&|,)");
Expand Down Expand Up @@ -131,6 +134,14 @@ public abstract class YouTubeExtractor extends AsyncTask<String, Void, SparseArr
FORMAT_MAP.put(249, new Format(249, "webm", Format.VCodec.NONE, Format.ACodec.OPUS, 48, true));
FORMAT_MAP.put(250, new Format(250, "webm", Format.VCodec.NONE, Format.ACodec.OPUS, 64, true));
FORMAT_MAP.put(251, new Format(251, "webm", Format.VCodec.NONE, Format.ACodec.OPUS, 160, true));

// HLS Live Stream
FORMAT_MAP.put(91, new Format(91, "mp4", 144 ,Format.VCodec.H264, Format.ACodec.AAC, 48, false, true));
FORMAT_MAP.put(92, new Format(92, "mp4", 240 ,Format.VCodec.H264, Format.ACodec.AAC, 48, false, true));
FORMAT_MAP.put(93, new Format(93, "mp4", 360 ,Format.VCodec.H264, Format.ACodec.AAC, 128, false, true));
FORMAT_MAP.put(94, new Format(94, "mp4", 480 ,Format.VCodec.H264, Format.ACodec.AAC, 128, false, true));
FORMAT_MAP.put(95, new Format(95, "mp4", 720 ,Format.VCodec.H264, Format.ACodec.AAC, 256, false, true));
FORMAT_MAP.put(96, new Format(96, "mp4", 1080 ,Format.VCodec.H264, Format.ACodec.AAC, 256, false, true));
}

public YouTubeExtractor(Context con) {
Expand Down Expand Up @@ -216,6 +227,45 @@ private SparseArray<YtFile> getStreamUrls() throws IOException, InterruptedExcep

parseVideoMeta(streamMap);

if(videoMeta.isLiveStream()){
mat = patHlsvp.matcher(streamMap);
if(mat.find()) {
String hlsvp = URLDecoder.decode(mat.group(1), "UTF-8");
SparseArray<YtFile> ytFiles = new SparseArray<>();

getUrl = new URL(hlsvp);
urlConnection = (HttpURLConnection) getUrl.openConnection();
urlConnection.setRequestProperty("User-Agent", USER_AGENT);
try {
reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if(line.startsWith("https://") || line.startsWith("http://")){
mat = patHlsItag.matcher(line);
if(mat.find()){
int itag = Integer.parseInt(mat.group(1));
YtFile newFile = new YtFile(FORMAT_MAP.get(itag), line);
ytFiles.put(itag, newFile);
}
}
}
} finally {
if (reader != null)
reader.close();
urlConnection.disconnect();
}

if (ytFiles.size() == 0) {
if (LOGGING)
Log.d(LOG_TAG, streamMap);
return null;
}
return ytFiles;
}
return null;
}


// Some videos are using a ciphered signature we need to get the
// deciphering js-file from the youtubepage.
if (streamMap == null || !streamMap.contains("use_cipher_signature=False")) {
Expand Down Expand Up @@ -539,12 +589,18 @@ private void parseDashManifest(String dashMpdUrl, SparseArray<YtFile> ytFiles) t
}

private void parseVideoMeta(String getVideoInfo) throws UnsupportedEncodingException {
boolean isLiveStream = false;
String title = null, author = null, channelId = null;
long viewCount = 0, length = 0;
Matcher mat = patTitle.matcher(getVideoInfo);
if (mat.find()) {
title = URLDecoder.decode(mat.group(1), "UTF-8");
}

mat = patHlsvp.matcher(getVideoInfo);
if(mat.find())
isLiveStream = true;

mat = patAuthor.matcher(getVideoInfo);
if (mat.find()) {
author = URLDecoder.decode(mat.group(1), "UTF-8");
Expand All @@ -561,7 +617,7 @@ private void parseVideoMeta(String getVideoInfo) throws UnsupportedEncodingExcep
if (mat.find()) {
viewCount = Long.parseLong(mat.group(1));
}
videoMeta = new VideoMeta(videoID, title, author, channelId, length, viewCount);
videoMeta = new VideoMeta(videoID, title, author, channelId, length, viewCount, isLiveStream);

}

Expand Down

0 comments on commit ab01318

Please sign in to comment.