Schnell und effizient: Dateien mit Java zu Vimeo exportieren
Erstellen Sie ein privates Vimeo-Video, speichern Sie seine Upload-Sitzung und setzen Sie eine unterbrochene Übertragung ab dem von Vimeo bestätigten Byte-Offset fort. Das folgende Java-Programm setzt mit dem HTTP-Client und Gson Vimeos Ablauf für fortsetzbare Uploads um, mit separaten Befehlen für die Erstellung, die Übertragung und den Verarbeitungsstatus.
Voraussetzungen
Diese Anleitung richtet sich an Linux-Systeme mit Bash, cURL und einer gepflegten Distribution des
JDK 21. Sie wurde mit OpenJDK 21.0.12.1 getestet;
verwenden Sie die aktuellen Sicherheitsupdates Ihres Distributors für die
gepflegte Java-21-Versionslinie. Setzen Sie
JAVA_HOME auf das Installationsverzeichnis dieses JDK.
Die Befehle wählen dessen Compiler und Laufzeitumgebung explizit aus.
Außerdem benötigen Sie eine Vimeo-Anwendung mit Upload-Zugriff, ein Benutzerzugriffstoken mit den
Scopes public, private und
upload (siehe Scopes) sowie ausreichend
verfügbares Kontingent im empfangenden Konto. Vergewissern Sie sich, dass diese Voraussetzungen
erfüllt sind, bevor Sie ein Testvideo erstellen. Verwenden Sie ein gültiges, nicht leeres Video,
das Sie hochladen dürfen, und führen Sie jeweils nur einen Dateiuploader mit dessen Sitzungsdatei aus.
Bewahren Sie Zugriffstokens auf dem Server auf. Auch die zurückgegebene Upload-URL ist sensibel: Speichern Sie sie privat und nehmen Sie sie nicht in Protokolle auf. Eine abgeschlossene Byte-Übertragung belegt weder, dass Vimeo das Video fertig transkodiert hat, noch, dass es öffentlich abspielbar ist.
Entwicklungsumgebung einrichten
Speichern Sie die vollständige Klasse unten in einem privaten Arbeitsverzeichnis als
VimeoUploader.java. Diese eigenständige Einrichtung lädt
Gson 2.14.0 herunter und kompiliert die Klasse.
Jeder Befehl muss erfolgreich abgeschlossen sein, bevor der nächste ausgeführt wird:
test -x "$JAVA_HOME/bin/java" &&
test -x "$JAVA_HOME/bin/javac" &&
command -v curl >/dev/null &&
curl --fail --location --output gson-2.14.0.jar \
https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.14.0/gson-2.14.0.jar &&
"$JAVA_HOME/bin/javac" -cp gson-2.14.0.jar VimeoUploader.java
Falls das Herunterladen oder Kompilieren fehlschlägt, beheben Sie das gemeldete Problem und führen Sie diesen Einrichtungsblock erneut aus. Er ersetzt die heruntergeladene JAR-Datei und erstellt die Klasse neu. Führen Sie nach einem fehlgeschlagenen Build keine ältere Klasse aus.
Anwendung konfigurieren und authentifizieren
Stellen Sie VIMEO_ACCESS_TOKEN über die Zugangsdatenverwaltung Ihres Servers bereit.
Der Befehl create sendet eine API-Anfrage und speichert eine private
Sitzungsdatei; resume verwendet diese Sitzung erneut, ohne ein weiteres
Video zu erstellen. Der Datei-Hash verhindert eine Fortsetzung mit anderen Inhalten gleicher Größe.
Ändern Sie die Eingabedatei während der gesamten Übertragung nicht.
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.HexFormat;
public final class VimeoUploader {
public record Session(String uri, String uploadLink, long size, String sha256) {}
private static final Gson JSON = new Gson();
private final HttpClient http;
private final URI api;
public VimeoUploader() {
this(HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(15))
.followRedirects(HttpClient.Redirect.NEVER).build(), URI.create("https://api.vimeo.com"));
}
// Tests supply a trusted HTTPS receiver instead of the Vimeo API.
VimeoUploader(HttpClient http, URI api) {
this.http = http;
this.api = https(api);
}
private static URI https(URI uri) {
if (!"https".equals(uri.getScheme()) || uri.getHost() == null
|| uri.getRawUserInfo() != null || uri.getFragment() != null) {
throw new IllegalArgumentException("An HTTPS URL without embedded credentials is required");
}
return uri;
}
private static String hash(Path file) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (var input = Files.newInputStream(file)) {
byte[] buffer = new byte[65536];
int count;
while ((count = input.read(buffer)) != -1) digest.update(buffer, 0, count);
}
return HexFormat.of().formatHex(digest.digest());
}
public Session create(Path file, String token, String name) throws Exception {
long size = Files.size(file);
if (!Files.isRegularFile(file) || size == 0) throw new IOException("A nonempty file is required");
if (token == null || token.isBlank()) throw new IllegalArgumentException("Missing Vimeo token");
String checksum = hash(file);
JsonObject upload = new JsonObject();
upload.addProperty("approach", "tus");
upload.addProperty("size", size);
JsonObject body = new JsonObject();
body.add("upload", upload);
body.addProperty("name", name);
JsonObject privacy = new JsonObject();
privacy.addProperty("view", "nobody");
body.add("privacy", privacy);
HttpResponse<String> response = http.send(HttpRequest.newBuilder(api.resolve("/me/videos"))
.timeout(Duration.ofSeconds(60)).header("Authorization", "Bearer " + token)
.header("Accept", "application/vnd.vimeo.*+json;version=3.4")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(JSON.toJson(body))).build(),
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) throw new IOException("Vimeo create HTTP " + response.statusCode());
JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject();
String uri = result.get("uri").getAsString();
if (!uri.matches("/videos/[0-9]+")) throw new IOException("Invalid video URI");
URI link = https(URI.create(result.getAsJsonObject("upload").get("upload_link").getAsString()));
return new Session(uri, link.toString(), size, checksum);
}
private static long offset(HttpResponse<?> response, long size) throws IOException {
if (!response.headers().firstValue("Tus-Resumable").orElse("").equals("1.0.0")) {
throw new IOException("Unexpected tus protocol version");
}
String value = response.headers().firstValue("Upload-Offset")
.orElseThrow(() -> new IOException("Missing upload offset"));
long offset;
try { offset = Long.parseLong(value); }
catch (NumberFormatException error) { throw new IOException("Invalid upload offset", error); }
if (offset < 0 || offset > size) throw new IOException("Upload offset out of range");
return offset;
}
public void resume(Path file, Session session) throws Exception {
if (session == null) {
throw new IOException("Missing upload session; reconcile your Vimeo account before creating another video");
}
if (Files.size(file) != session.size() || !hash(file).equals(session.sha256())) {
throw new IOException("The upload file changed");
}
URI link = https(URI.create(session.uploadLink()));
HttpResponse<Void> head = http.send(HttpRequest.newBuilder(link)
.timeout(Duration.ofSeconds(30)).header("Tus-Resumable", "1.0.0")
.header("Accept", "application/vnd.vimeo.*+json;version=3.4")
.method("HEAD", HttpRequest.BodyPublishers.noBody()).build(),
HttpResponse.BodyHandlers.discarding());
if (head.statusCode() != 200 && head.statusCode() != 204) {
throw new IOException("Upload HEAD HTTP " + head.statusCode());
}
long position = offset(head, session.size());
try (RandomAccessFile input = new RandomAccessFile(file.toFile(), "r")) {
input.seek(position);
byte[] buffer = new byte[1024 * 1024];
while (position < session.size()) {
int length = (int) Math.min(buffer.length, session.size() - position);
input.readFully(buffer, 0, length);
HttpResponse<Void> patch = http.send(HttpRequest.newBuilder(link)
.timeout(Duration.ofMinutes(2)).header("Tus-Resumable", "1.0.0")
.header("Accept", "application/vnd.vimeo.*+json;version=3.4")
.header("Upload-Offset", Long.toString(position))
.header("Content-Type", "application/offset+octet-stream")
.method("PATCH", HttpRequest.BodyPublishers.ofByteArray(buffer, 0, length)).build(),
HttpResponse.BodyHandlers.discarding());
if (patch.statusCode() != 204) throw new IOException("Upload PATCH HTTP " + patch.statusCode());
long confirmed = offset(patch, session.size());
if (confirmed != position + length) throw new IOException("Unexpected confirmed offset");
position = confirmed;
}
}
}
public JsonObject status(Session session, String token) throws Exception {
if (session == null || session.uri() == null || !session.uri().matches("/videos/[0-9]+")) {
throw new IOException("Missing or invalid video URI");
}
if (token == null || token.isBlank()) throw new IllegalArgumentException("Missing Vimeo token");
HttpResponse<String> response = http.send(HttpRequest.newBuilder(api.resolve(session.uri()
+ "?fields=uri,upload.status,transcode.status,privacy.view"))
.timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + token)
.header("Accept", "application/vnd.vimeo.*+json;version=3.4")
.GET().build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) throw new IOException("Vimeo status HTTP " + response.statusCode());
JsonObject video = JsonParser.parseString(response.body()).getAsJsonObject();
if (!session.uri().equals(video.get("uri").getAsString())) {
throw new IOException("Unexpected status video URI");
}
JsonObject result = new JsonObject();
result.add("uri", video.get("uri"));
result.addProperty("upload", statusValue(video, "upload"));
result.addProperty("transcode", statusValue(video, "transcode"));
result.add("privacy", video.getAsJsonObject("privacy").get("view"));
return result;
}
private static String statusValue(JsonObject video, String field) {
if (!video.has(field) || !video.get(field).isJsonObject()) return "unavailable";
var value = video.getAsJsonObject(field).get("status");
return value == null || value.isJsonNull() ? "unavailable" : value.getAsString();
}
public static void main(String[] args) throws Exception {
if (args.length != 3) throw new IllegalArgumentException("create|resume|status video-file session-file");
for (int i = 1; i < args.length; i++) {
if (!args[i].chars().allMatch(c -> c >= 32 && c <= 126)) {
throw new IllegalArgumentException("Use printable ASCII paths for this command-line example");
}
}
Path file = Path.of(args[1]);
Path state = Path.of(args[2]);
VimeoUploader uploader = new VimeoUploader();
if (args[0].equals("create")) {
Files.createFile(state, PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")));
Session session = uploader.create(file, System.getenv("VIMEO_ACCESS_TOKEN"), file.getFileName().toString());
Files.writeString(state, JSON.toJson(session));
System.out.println("Created " + session.uri());
} else if (args[0].equals("resume")) {
Session session = JSON.fromJson(Files.readString(state), Session.class);
uploader.resume(file, session);
System.out.println("Bytes transferred for " + session.uri());
} else if (args[0].equals("status")) {
Session session = JSON.fromJson(Files.readString(state), Session.class);
System.out.println(JSON.toJson(uploader.status(session, System.getenv("VIMEO_ACCESS_TOKEN"))));
} else {
throw new IllegalArgumentException("Choose create, resume, or status");
}
}
}
Die Sitzungsdatei wird exklusiv mit Zugriffsrechten nur für den Eigentümer erstellt. Eine vorhandene
Datei wird von create niemals überschrieben. Schützen Sie auch ihr
Verzeichnis und setzen Sie nur Sitzungen fort, die Ihre Anwendung erstellt hat: Deren Upload-URLs
können Ihr Video empfangen. Dieses Befehlszeilenbeispiel akzeptiert druckbare ASCII-Pfade, um eine
mehrdeutige Decodierung nativer Dateinamen zu vermeiden. Benennen Sie andere Pfade vor der Verwendung
um und setzen Sie Pfade mit Leerzeichen oder Shell-Zeichen in Anführungszeichen.
Fehlerbehandlung und Wiederholungsversuche
Wiederholen Sie create nicht blind: Geht eine Antwort verloren, kann ein
Video auf Vimeo verbleiben, obwohl beim Aufrufer eine Zeitüberschreitung auftrat. Ist die
Sitzungsdatei nach einem Fehler leer, gleichen Sie den Zustand im Konto ab, bevor Sie ein Ersatzvideo
erstellen. Führen Sie nach einem Übertragungsfehler resume mit derselben
Sitzung und unveränderter Eingabe erneut aus. Die Anfrage mit HEAD liest
den vom Server bestätigten Offset aus, einschließlich der Bytes, die vor einer verlorenen Antwort
angenommen wurden.
Authentifizierungsfehler und ungültige Anfragen müssen korrigiert werden. Beachten Sie bei einer
API-Drosselung Vimeos Hinweise zu Anfragelimits und den
Antwort-Header X-RateLimit-Reset. Warten Sie bei vorübergehenden Dienst- oder
Netzwerkfehlern vor dem Fortsetzen mit zunehmenden Abständen.
Eine abgelaufene Upload-URL erfordert eine explizite Wiederherstellung; lösen Sie dadurch nicht
automatisch eine Anfrage für ein neues Video aus. Das Beispiel fügt das Bearer-Token nur
API-Anfragen hinzu und fordert beim Erstellen des Videos privacy.view: nobody an.
Es veröffentlicht das Video nicht. Mit seinen Blöcken von 1 MiB lässt sich das Beispiel leicht
unterbrechen; lesen Sie bei größeren Uploads Vimeos Empfehlungen zur Blockgröße, bevor Sie den
Durchsatz optimieren.
Implementierung testen
Legen Sie Ihr Video unter video.mp4 ab oder ersetzen Sie den Pfad in jedem
Befehl. Wenn VIMEO_ACCESS_TOKEN in der Umgebung verfügbar ist, erstellen Sie das
private Video einmal:
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader create "video.mp4" "session.json"
Übertragen Sie anschließend seine Bytes. Falls der Prozess stoppt oder eine Anfrage fehlschlägt, führen Sie denselben Befehl mit derselben Datei und Sitzung erneut aus:
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader resume "video.mp4" "session.json"
Die Erfolgsmeldung nennt das Video, dessen Bytes übertragen wurden. Jede Fortsetzung beginnt mit
einer HEAD-Anfrage über tus, die beide Header aus Vimeos HEAD-Tabelle enthält:
Tus-Resumable: 1.0.0 und Accept: application/vnd.vimeo.*+json;version=3.4. PATCH sendet nur die Bytes nach
dem bestätigten Offset. Das Beispiel sendet bei PATCH außerdem Accept,
wie in Vimeos Anleitung zum Fortsetzen beschrieben.
Prüfen Sie das empfangende Video separat:
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader status "video.mp4" "session.json"
Dies gibt eine JSON-Momentaufnahme mit der Video-URI und den Werten für
upload, transcode und
privacy von Vimeo aus. Ein Upload-Status von
complete dokumentiert die Annahme durch den Anbieter; ein Transkodierungsstatus
von complete dokumentiert den Abschluss der Verarbeitung. Keiner der beiden
Status macht dieses private Video öffentlich abspielbar. Wiederholen Sie
status später, falls die Verarbeitung noch läuft. Beim Warten auf
Antwort-Header tritt eine Zeitüberschreitung nach 60 Sekunden für die Erstellung, nach 30 Sekunden
für HEAD oder Status und nach zwei Minuten pro PATCH auf. Das Lesen eines Antwortkörpers kann länger
als diese Zeitspanne dauern. Das Programm hat weder ein Zeitlimit für den gesamten Upload noch eine
automatische Wiederholungsschleife; verwenden Sie bei Bedarf ein separates Zeitlimit für die Aufgabe
in Ihrem Dienst.
Bewährte Verfahren für Video-Uploads
Um gespeicherte Inhalte zu prüfen, laden Sie die ursprüngliche Quelldatei herunter und vergleichen
Sie jedes Byte mit Ihrer Eingabe. Vimeos API-Dateilinks erfordern
die Token-Scopes public, private und
video_files sowie einen dafür berechtigten Kontotarif.
Die Verfügbarkeit des Originals hängt außerdem von der
Speicherung der Quelldatei ab. Diese erfordert einen kostenpflichtigen
Tarif und muss vor dem Hochladen aktiviert werden. Eine transkodierte Variante ist eine andere
Datei und kann keine Byte-für-Byte-Übereinstimmung mit dem Original belegen. Bewahren Sie Ihr eigenes
Original auf und löschen Sie das private Testvideo nach Abschluss der Tests aus Ihrem Vimeo-Konto.
Workflows mit Transloadits Robot für den Vimeo-Export verbessern
Verwenden Sie 🤖 /video/encode zum Encoding und zum Hinzufügen von Wasserzeichen. Übergeben Sie dessen Ausgabe anschließend an 🤖 /vimeo/store für den Export. Der Export-Robot führt selbst nicht alle zuvor genannten Videotransformationen aus.
Fazit
Bewahren Sie die gespeicherte Sitzung auf, bis Sie das empfangende Video geprüft und alle nötigen Wiederherstellungsmaßnahmen abgeschlossen haben. Für eine verwaltete Verarbeitung vor dem Export nutzen Sie Transloadits Video-Encoding-Dienst.
