Schnell und effizient: Dateien in Java zu Vimeo exportieren
Vimeos aktueller Ablauf für fortsetzbare Uploads erstellt ein Video über
POST /me/videos, wobei upload.approach auf
tus gesetzt wird. Anschließend sendet er die Bytes an die zurückgegebene
upload.upload_link. Dies ersetzt das bisherige Beispiel mit
com.clickntap:vimeo:2.0. Die folgende Implementierung nutzt den HTTP-Client von Java und
Gson. Sie orientiert sich an der Upload-Anleitung von Vimeo,
der von Vimeo gepflegten Python-Implementierung für Uploads
und dem tus-Protokoll.
Voraussetzungen
Verwenden Sie JDK 21, eine Vimeo-Anwendung mit Upload-Berechtigung
und ein Benutzerzugriffstoken mit den erforderlichen Upload-Berechtigungen. Klären Sie mit Vimeo,
ob Ihre Anwendung Upload-Zugriff hat und welches Kontingent Ihrem Konto zur Verfügung steht.
Diese Anleitung setzt weder einen bestimmten kostenpflichtigen Tarif noch eine bestimmte
Dateigrößen- oder Dauerbegrenzung voraus. Diese kontospezifischen Bedingungen müssen aktuell
am Live-System geprüft werden.
Bewahren Sie Zugriffstoken auf dem Server auf. Auch die zurückgegebene Upload-URL ist vertraulich: Speichern Sie sie geschützt und nehmen Sie sie nicht in Protokolle auf. Eine abgeschlossene Byte-Übertragung bedeutet weder, dass Vimeo das Video vollständig transkodiert hat, noch, dass es öffentlich abspielbar ist.
Entwicklungsumgebung einrichten
Speichern Sie die folgende Java-Klasse als VimeoUploader.java.
Dieses eigenständige Beispiel legt Gson auf Version 2.14.0 fest:
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
javac -cp gson-2.14.0.jar VimeoUploader.java
Verwenden Sie für Maven com.google.code.gson:gson:2.14.0.
Die Klasse nutzt kein inoffizielles Vimeo-SDK.
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, dass die Übertragung mit anderen Inhalten derselben
Größe fortgesetzt wird. Verä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"));
}
// A trusted alternate HTTPS origin permits local protocol verification.
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);
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")
.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("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 static void main(String[] args) throws Exception {
if (args.length != 3) throw new IllegalArgumentException("create|resume video-file session-file");
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 {
throw new IllegalArgumentException("Choose create or resume");
}
}
}
Das Beispiel für die private Datei ist für macOS und Linux ausgelegt. Konfigurieren Sie unter Windows eine ebenso restriktive ACL, bevor Sie die Sitzung dauerhaft speichern. Schützen Sie auch das Verzeichnis. Setzen Sie Sitzungen nur mit Dateien fort, die Ihre Anwendung erstellt hat. Sie enthalten eine Ziel-URL, die Ihr Video empfangen kann.
Fehlerbehandlung und erneute Versuche
Wiederholen Sie create nicht unbesehen: Geht eine Antwort verloren, kann
auf Vimeo bereits ein Video vorhanden sein, obwohl der Aufrufer eine Zeitüberschreitung erhalten
hat. Wenn die Sitzungsdatei nach einem Fehler leer ist, prüfen Sie, ob die Anfrage in Ihrem
Vimeo-Konto bereits ein Video erstellt hat, bevor Sie ein Ersatzvideo erstellen.
Führen Sie nach einem Übertragungsfehler
resume mit derselben Sitzung und unveränderter Eingabedatei erneut aus.
Die zugehörige Anfrage mit HEAD liest den vom Server bestätigten Offset
aus. Dieser berücksichtigt auch Bytes, die vor dem Verlust einer Antwort angenommen wurden.
Authentifizierungsfehler und ungültige Anfragen müssen behoben werden. Beachten Sie bei einer
Drosselung Vimeos Retry-After oder die Hinweise zu Anfragelimits. Warten Sie bei
vorübergehenden Dienst- oder Netzwerkfehlern vor der Fortsetzung zunächst ab. Eine abgelaufene
Upload-URL erfordert eine explizite Wiederherstellung. Die abgelaufene Upload-URL darf keine automatische
Anfrage zum Erstellen eines neuen Videos auslösen. Das Beispiel fügt der tus-Upload-URL niemals
das Vimeo-Bearer-Token hinzu und ändert niemals die Datenschutzeinstellungen.
Implementierung testen
Nach der Autorisierung des Kontos erstellen diese Befehle ein Video und übertragen seine Bytes:
java -cp '.:gson-2.14.0.jar' VimeoUploader create video.mp4 session.json
java -cp '.:gson-2.14.0.jar' VimeoUploader resume video.mp4 session.json
Lokale HTTPS-Fixtures können die JSON-Anfrage, Offsets für die Fortsetzung, Bytes der Datenblöcke, fehlende Header und Fehlerstatus prüfen. Sie können weder den Upload-Zugriff Ihrer Anwendung noch das Kontingent, das Transkodierungsergebnis oder die Wiedergabe bestätigen. Prüfen Sie diese Punkte vor der Bereitstellung mit einem ausdrücklich autorisierten Live-Upload.
Bewährte Verfahren für Video-Uploads
Verändern Sie das Video während des Uploads nicht, speichern Sie die Video-URI dauerhaft für spätere Statusabfragen und behandeln Sie Sitzungs-URLs wie Zugangsdaten. Prüfen Sie Vimeos aktuelle Konto- und Upload-Dokumentation, statt Tarifnamen oder maximale Dateigrößen fest im Code zu hinterlegen.
Workflows mit dem Vimeo-Export-Robot von Transloadit verbessern
Nutzen Sie 🤖 /video/encode zum Encoding und zum Hinzufügen von Wasserzeichen. Übergeben Sie anschließend die Ausgabe an 🤖 /vimeo/store für den Export. Der Export-Robot führt selbst nicht alle vorhergehenden Videotransformationen aus.
Fazit
Erstellen Sie ein Vimeo-Video, bewahren Sie seine Upload-Sitzung auf und setzen Sie die Übertragung am bestätigten tus-Offset fort. Für eine verwaltete Verarbeitung vor dem Export nutzen Sie den Dienst für Video-Encoding von Transloadit.
