Skip to content

Commit 588a7a6

Browse files
freyacodestopi314
authored andcommitted
Merge pull request #149 from lavalink-devs/fix/vimeo-playback
1 parent 5492616 commit 588a7a6

File tree

3 files changed

+172
-57
lines changed

3 files changed

+172
-57
lines changed

main/src/main/java/com/sedmelluq/discord/lavaplayer/source/vimeo/VimeoAudioSourceManager.java

+96-8
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22

33
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
44
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;
5-
import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools;
6-
import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools;
7-
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
8-
import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser;
5+
import com.sedmelluq.discord.lavaplayer.tools.*;
96
import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools;
107
import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable;
118
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface;
@@ -19,14 +16,18 @@
1916
import org.apache.http.client.config.RequestConfig;
2017
import org.apache.http.client.methods.CloseableHttpResponse;
2118
import org.apache.http.client.methods.HttpGet;
19+
import org.apache.http.client.methods.HttpUriRequest;
20+
import org.apache.http.client.utils.URIBuilder;
2221
import org.apache.http.impl.client.HttpClientBuilder;
2322

2423
import java.io.DataInput;
2524
import java.io.DataOutput;
2625
import java.io.IOException;
26+
import java.net.URISyntaxException;
2727
import java.nio.charset.StandardCharsets;
2828
import java.util.function.Consumer;
2929
import java.util.function.Function;
30+
import java.util.regex.Matcher;
3031
import java.util.regex.Pattern;
3132

3233
import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS;
@@ -35,7 +36,7 @@
3536
* Audio source manager which detects Vimeo tracks by URL.
3637
*/
3738
public class VimeoAudioSourceManager implements AudioSourceManager, HttpConfigurable {
38-
private static final String TRACK_URL_REGEX = "^https://vimeo.com/[0-9]+(?:\\?.*|)$";
39+
private static final String TRACK_URL_REGEX = "^https?://vimeo.com/([0-9]+)(?:\\?.*|)$";
3940
private static final Pattern trackUrlPattern = Pattern.compile(TRACK_URL_REGEX);
4041

4142
private final HttpInterfaceManager httpInterfaceManager;
@@ -54,13 +55,15 @@ public String getSourceName() {
5455

5556
@Override
5657
public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) {
57-
if (!trackUrlPattern.matcher(reference.identifier).matches()) {
58+
Matcher trackUrl = trackUrlPattern.matcher(reference.identifier);
59+
60+
if (!trackUrl.matches()) {
5861
return null;
5962
}
6063

6164
try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) {
62-
return loadFromTrackPage(httpInterface, reference.identifier);
63-
} catch (IOException e) {
65+
return loadVideoFromApi(httpInterface, trackUrl.group(1));
66+
} catch (IOException | URISyntaxException e) {
6467
throw new FriendlyException("Loading Vimeo track information failed.", SUSPICIOUS, e);
6568
}
6669
}
@@ -85,6 +88,10 @@ public void shutdown() {
8588
ExceptionTools.closeWithWarnings(httpInterfaceManager);
8689
}
8790

91+
public HttpInterfaceManager getHttpInterfaceManager() {
92+
return httpInterfaceManager;
93+
}
94+
8895
/**
8996
* @return Get an HTTP interface for a playing track.
9097
*/
@@ -143,4 +150,85 @@ private AudioTrack loadTrackFromPageContent(String trackUrl, String content) thr
143150
trackUrl
144151
), this);
145152
}
153+
154+
private AudioTrack loadVideoFromApi(HttpInterface httpInterface, String videoId) throws IOException, URISyntaxException {
155+
JsonBrowser videoData = getVideoFromApi(httpInterface, videoId);
156+
157+
AudioTrackInfo info = new AudioTrackInfo(
158+
videoData.get("name").text(),
159+
videoData.get("uploader").get("name").textOrDefault("Unknown artist"),
160+
Units.secondsToMillis(videoData.get("duration").asLong(Units.DURATION_SEC_UNKNOWN)),
161+
videoId,
162+
false,
163+
"https://vimeo.com/" + videoId
164+
);
165+
166+
return new VimeoAudioTrack(info, this);
167+
}
168+
169+
public JsonBrowser getVideoFromApi(HttpInterface httpInterface, String videoId) throws IOException, URISyntaxException {
170+
String jwt = getApiJwt(httpInterface);
171+
172+
URIBuilder builder = new URIBuilder("https://api.vimeo.com/videos/" + videoId);
173+
// adding `play` to the fields achieves the same thing as requesting the config_url, but with one less request.
174+
// maybe we should consider using that instead? Need to figure out what the difference is, if any.
175+
builder.setParameter("fields", "config_url,name,uploader.name,duration,pictures");
176+
177+
HttpUriRequest request = new HttpGet(builder.build());
178+
request.setHeader("Authorization", "jwt " + jwt);
179+
request.setHeader("Accept", "application/json");
180+
181+
try (CloseableHttpResponse response = httpInterface.execute(request)) {
182+
HttpClientTools.assertSuccessWithContent(response, "fetch video api");
183+
return JsonBrowser.parse(response.getEntity().getContent());
184+
}
185+
}
186+
187+
public PlaybackFormat getPlaybackFormat(HttpInterface httpInterface, String configUrl) throws IOException {
188+
try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(configUrl))) {
189+
HttpClientTools.assertSuccessWithContent(response, "fetch playback formats");
190+
191+
JsonBrowser json = JsonBrowser.parse(response.getEntity().getContent());
192+
193+
// {"dash", "hls", "progressive"}
194+
// N.B. opus is referenced in some of the URLs, but I don't see any formats offering opus audio codec.
195+
// Might be a gradual rollout so this may need revisiting.
196+
JsonBrowser files = json.get("request").get("files");
197+
198+
if (!files.get("progressive").isNull()) {
199+
JsonBrowser progressive = files.get("progressive").index(0);
200+
201+
if (!progressive.isNull()) {
202+
return new PlaybackFormat(progressive.get("url").text(), false);
203+
}
204+
}
205+
206+
if (!files.get("hls").isNull()) {
207+
JsonBrowser hls = files.get("hls");
208+
// ["akfire_interconnect_quic", "fastly_skyfire"]
209+
JsonBrowser cdns = hls.get("cdns");
210+
return new PlaybackFormat(cdns.get(hls.get("default_cdn").text()).get("url").text(), true);
211+
}
212+
213+
throw new RuntimeException("No supported formats");
214+
}
215+
}
216+
217+
private String getApiJwt(HttpInterface httpInterface) throws IOException {
218+
try (CloseableHttpResponse response = httpInterface.execute(new HttpGet("https://vimeo.com/_next/viewer"))) {
219+
HttpClientTools.assertSuccessWithContent(response, "fetch jwt");
220+
JsonBrowser json = JsonBrowser.parse(response.getEntity().getContent());
221+
return json.get("jwt").text();
222+
}
223+
}
224+
225+
public static class PlaybackFormat {
226+
public final String url;
227+
public final boolean isHls;
228+
229+
public PlaybackFormat(String url, boolean isHls) {
230+
this.url = url;
231+
this.isHls = isHls;
232+
}
233+
}
146234
}

main/src/main/java/com/sedmelluq/discord/lavaplayer/source/vimeo/VimeoAudioTrack.java

+61-49
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.sedmelluq.discord.lavaplayer.source.vimeo;
22

33
import com.sedmelluq.discord.lavaplayer.container.mpeg.MpegAudioTrack;
4+
import com.sedmelluq.discord.lavaplayer.container.playlists.ExtendedM3uParser;
5+
import com.sedmelluq.discord.lavaplayer.container.playlists.HlsStreamTrack;
46
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;
57
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
68
import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser;
@@ -20,6 +22,7 @@
2022
import java.io.IOException;
2123
import java.net.URI;
2224
import java.nio.charset.StandardCharsets;
25+
import java.util.Objects;
2326

2427
import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS;
2528

@@ -32,7 +35,7 @@ public class VimeoAudioTrack extends DelegatedAudioTrack {
3235
private final VimeoAudioSourceManager sourceManager;
3336

3437
/**
35-
* @param trackInfo Track info
38+
* @param trackInfo Track info
3639
* @param sourceManager Source manager which was used to find this track
3740
*/
3841
public VimeoAudioTrack(AudioTrackInfo trackInfo, VimeoAudioSourceManager sourceManager) {
@@ -41,61 +44,70 @@ public VimeoAudioTrack(AudioTrackInfo trackInfo, VimeoAudioSourceManager sourceM
4144
this.sourceManager = sourceManager;
4245
}
4346

44-
@Override
45-
public void process(LocalAudioTrackExecutor localExecutor) throws Exception {
46-
try (HttpInterface httpInterface = sourceManager.getHttpInterface()) {
47-
String playbackUrl = loadPlaybackUrl(httpInterface);
48-
49-
log.debug("Starting Vimeo track from URL: {}", playbackUrl);
50-
51-
try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(playbackUrl), null)) {
52-
processDelegate(new MpegAudioTrack(trackInfo, stream), localExecutor);
53-
}
47+
@Override
48+
public void process(LocalAudioTrackExecutor localExecutor) throws Exception {
49+
try (HttpInterface httpInterface = sourceManager.getHttpInterface()) {
50+
JsonBrowser videoData = sourceManager.getVideoFromApi(httpInterface, trackInfo.identifier);
51+
VimeoAudioSourceManager.PlaybackFormat playbackFormat = sourceManager.getPlaybackFormat(httpInterface, videoData.get("config_url").text());
52+
53+
log.debug("Starting Vimeo track. HLS: {}, URL: {}", playbackFormat.isHls, playbackFormat.url);
54+
55+
if (playbackFormat.isHls) {
56+
processDelegate(
57+
new HlsStreamTrack(trackInfo, extractHlsAudioPlaylistUrl(httpInterface, playbackFormat.url), sourceManager.getHttpInterfaceManager(), true),
58+
localExecutor
59+
);
60+
} else {
61+
try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(playbackFormat.url), null)) {
62+
processDelegate(new MpegAudioTrack(trackInfo, stream), localExecutor);
63+
}
64+
}
65+
}
5466
}
55-
}
56-
57-
private String loadPlaybackUrl(HttpInterface httpInterface) throws IOException {
58-
JsonBrowser config = loadPlayerConfig(httpInterface);
59-
if (config == null) {
60-
throw new FriendlyException("Track information not present on the page.", SUSPICIOUS, null);
61-
}
62-
63-
String trackConfigUrl = config.get("player").get("config_url").text();
64-
JsonBrowser trackConfig = loadTrackConfig(httpInterface, trackConfigUrl);
65-
66-
return trackConfig.get("request").get("files").get("progressive").index(0).get("url").text();
67-
}
6867

69-
private JsonBrowser loadPlayerConfig(HttpInterface httpInterface) throws IOException {
70-
try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(trackInfo.identifier))) {
71-
int statusCode = response.getStatusLine().getStatusCode();
68+
protected String resolveRelativeUrl(String baseUrl, String url) {
69+
while (url.startsWith("../")) {
70+
url = url.substring(3);
71+
baseUrl = baseUrl.substring(0, baseUrl.lastIndexOf('/'));
72+
}
7273

73-
if (!HttpClientTools.isSuccessWithContent(statusCode)) {
74-
throw new FriendlyException("Server responded with an error.", SUSPICIOUS,
75-
new IllegalStateException("Response code for player config is " + statusCode));
76-
}
77-
78-
return sourceManager.loadConfigJsonFromPageContent(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
74+
return baseUrl + ((url.startsWith("/")) ? url : "/" + url);
7975
}
80-
}
81-
82-
private JsonBrowser loadTrackConfig(HttpInterface httpInterface, String trackAccessInfoUrl) throws IOException {
83-
try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(trackAccessInfoUrl))) {
84-
int statusCode = response.getStatusLine().getStatusCode();
85-
86-
if (!HttpClientTools.isSuccessWithContent(statusCode)) {
87-
throw new FriendlyException("Server responded with an error.", SUSPICIOUS,
88-
new IllegalStateException("Response code for track access info is " + statusCode));
89-
}
9076

91-
return JsonBrowser.parse(response.getEntity().getContent());
77+
/** Vimeo HLS uses separate audio and video. This extracts the audio playlist URL from EXT-X-MEDIA */
78+
private String extractHlsAudioPlaylistUrl(HttpInterface httpInterface, String videoPlaylistUrl) throws IOException {
79+
String url = null;
80+
try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(videoPlaylistUrl))) {
81+
int statusCode = response.getStatusLine().getStatusCode();
82+
83+
if (!HttpClientTools.isSuccessWithContent(statusCode)) {
84+
throw new FriendlyException("Server responded with an error.", SUSPICIOUS,
85+
new IllegalStateException("Response code for track access info is " + statusCode));
86+
}
87+
88+
String bodyString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
89+
for (String rawLine : bodyString.split("\n")) {
90+
ExtendedM3uParser.Line line = ExtendedM3uParser.parseLine(rawLine);
91+
92+
if (Objects.equals(line.directiveName, "EXT-X-MEDIA") && Objects.equals(line.directiveArguments.get("TYPE"), "AUDIO")) {
93+
url = line.directiveArguments.get("URI");
94+
break;
95+
}
96+
}
97+
}
98+
99+
if (url == null) {
100+
throw new FriendlyException("Failed to find audio playlist URL.", SUSPICIOUS,
101+
new IllegalStateException("Valid audio directive was not found"));
102+
}
103+
104+
return resolveRelativeUrl(videoPlaylistUrl.substring(0, videoPlaylistUrl.lastIndexOf('/')), url);
92105
}
93-
}
94106

95-
@Override
96-
protected AudioTrack makeShallowClone() {
97-
return new VimeoAudioTrack(trackInfo, sourceManager);
98-
}
107+
@Override
108+
protected AudioTrack makeShallowClone() {
109+
return new VimeoAudioTrack(trackInfo, sourceManager);
110+
}
99111

100112
@Override
101113
public AudioSourceManager getSourceManager() {

main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/JsonBrowser.java

+15
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.io.IOException;
1212
import java.io.InputStream;
1313
import java.util.ArrayList;
14+
import java.util.Collections;
1415
import java.util.List;
1516

1617
/**
@@ -103,6 +104,20 @@ public List<JsonBrowser> values() {
103104
}
104105

105106
/**
107+
* Returns a list of all key names in this element if it's a map.
108+
* @return The list of keys.
109+
*/
110+
public List<String> keys() {
111+
if (!isMap()) {
112+
return Collections.emptyList();
113+
}
114+
115+
List<String> keys = new ArrayList<>();
116+
node.fieldNames().forEachRemaining(keys::add);
117+
return keys;
118+
}
119+
120+
/**
106121
* Attempt to retrieve the value in the specified format
107122
*
108123
* @param klass The class to retrieve the value as

0 commit comments

Comments
 (0)