Exporter vite et efficacement des fichiers vers Vimeo en Java
Créez une seule vidéo privée sur Vimeo, enregistrez sa session de téléversement et reprenez un transfert interrompu au décalage d’octets confirmé par Vimeo. Le programme Java ci-dessous utilise le client HTTP et Gson pour implémenter la procédure de téléversement avec reprise de Vimeo, avec des commandes distinctes pour la création, le transfert et l’état du traitement.
Prérequis
Ce guide cible Linux avec Bash, cURL et une distribution maintenue du JDK
21. Il a été testé avec OpenJDK 21.0.12.1 ; appliquez
les mises à jour de sécurité actuelles de votre distributeur pour la
branche Java 21 maintenue. Définissez
JAVA_HOME sur le répertoire d’installation de ce JDK. Les commandes sélectionnent
explicitement son compilateur et son environnement d’exécution.
Vous avez également besoin d’une application Vimeo autorisée à téléverser, d’un jeton d’accès
utilisateur avec les portées public, private et
upload (détails des portées), et d’une capacité
suffisante dans le quota du compte destinataire. Vérifiez ces prérequis avant de créer une vidéo de
test. Utilisez une vidéo valide, non vide, que vous avez le droit de téléverser, et n’exécutez qu’un
seul outil de téléversement à la fois pour son fichier de session.
Conservez les jetons d’accès sur le serveur. L’URL de téléversement renvoyée est également sensible : stockez-la de manière privée et ne l’inscrivez pas dans les journaux. Un transfert d’octets terminé ne prouve ni que Vimeo a fini de transcoder la vidéo, ni qu’elle est lisible publiquement.
Configurer votre environnement de développement
Dans un répertoire de travail privé, enregistrez la classe complète ci-dessous sous le nom
VimeoUploader.java. Cette configuration autonome télécharge
Gson 2.14.0 et compile la classe. Chaque commande doit réussir avant
l’exécution de la suivante :
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
Si le téléchargement ou la compilation échoue, corrigez le problème signalé et réexécutez ce bloc de configuration. Il remplace le JAR téléchargé et recompile la classe ; n’exécutez pas une ancienne classe après un échec de compilation.
Configurer et authentifier votre application
Fournissez VIMEO_ACCESS_TOKEN via le système de gestion des secrets de votre serveur.
La commande create effectue une seule requête API et enregistre un fichier de
session privé ; resume réutilise cette session sans créer d’autre vidéo.
L’empreinte du fichier empêche une reprise avec un contenu différent de même taille. Veillez à ce
que le fichier d’entrée reste inchangé pendant tout le transfert.
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");
}
}
}
Le fichier de session est créé en mode exclusif, avec des permissions réservées à son propriétaire.
La commande create n’écrase jamais un fichier existant. Protégez également son
répertoire et ne reprenez que les sessions créées par votre application : leurs URL de téléversement
peuvent recevoir votre vidéo. Cet exemple en ligne de commande accepte les chemins composés de
caractères ASCII imprimables pour éviter tout décodage ambigu des noms de fichiers natifs. Renommez
les autres chemins avant utilisation et placez entre guillemets ceux contenant des espaces ou des
caractères interprétés par le shell.
Gérer les erreurs et les nouvelles tentatives
Ne relancez pas create à l’aveugle : une réponse perdue peut laisser une vidéo
sur Vimeo alors que l’appelant a constaté un dépassement de délai. Si le fichier de session est vide
après un échec, vérifiez et rapprochez l’état des ressources du compte avant de créer une vidéo de
remplacement. Après un échec de transfert, réexécutez resume avec la même
session et le fichier d’entrée inchangé. Sa requête HEAD lit le décalage
confirmé par le serveur, y compris les octets acceptés avant la perte d’une réponse.
Les échecs d’authentification et les requêtes invalides nécessitent une correction. En cas de
limitation du débit des requêtes API, suivez les
consignes de Vimeo sur les limites de débit et l’en-tête de réponse
X-RateLimit-Reset. En cas de défaillance temporaire du service ou du réseau, attendez
avant de reprendre. Une URL de téléversement expirée nécessite une procédure de récupération
explicite ; ne déclenchez pas automatiquement une requête de création de vidéo dans ce cas.
L’exemple joint le jeton porteur uniquement aux requêtes API et demande
privacy.view: nobody lors de la création de la vidéo. Il ne publie pas la vidéo. Ses blocs
de 1 MiB facilitent l’interruption de l’exemple ; pour les téléversements plus volumineux, consultez
les recommandations de Vimeo sur la taille des blocs avant d’optimiser le débit.
Tester votre implémentation
Placez votre vidéo à l’emplacement video.mp4, ou remplacez ce chemin par le sien
dans chaque commande. Avec VIMEO_ACCESS_TOKEN disponible dans l’environnement, créez la
vidéo privée une seule fois :
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader create "video.mp4" "session.json"
Transférez ensuite ses octets. Si le processus s’arrête ou qu’une requête échoue, réexécutez cette même commande avec le même fichier et la même session :
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader resume "video.mp4" "session.json"
Le message de réussite identifie la vidéo dont les octets ont été transférés. Chaque reprise
commence par une requête tus HEAD contenant les deux en-têtes du tableau HEAD de Vimeo :
Tus-Resumable: 1.0.0 et Accept: application/vnd.vimeo.*+json;version=3.4. PATCH envoie uniquement les octets
situés après le décalage confirmé. L’exemple envoie également Accept avec
PATCH, comme indiqué dans les instructions de reprise de Vimeo.
Vérifiez séparément la vidéo reçue :
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader status "video.mp4" "session.json"
Cette commande affiche un instantané JSON contenant l’URI de la vidéo et les valeurs
upload, transcode et privacy
renvoyées par Vimeo. Un état de téléversement complete indique l’acceptation par
le fournisseur ; un état de transcodage complete indique la fin du traitement.
Aucun des deux ne rend cette vidéo privée lisible publiquement. Réexécutez
status plus tard si le traitement est toujours en cours. Les requêtes
atteignent leur délai d’attente des en-têtes de réponse après 60 secondes pour la création,
30 secondes pour HEAD ou la consultation de l’état, et deux minutes par PATCH. La lecture du corps
d’une réponse peut prendre plus de temps que ce délai. Le programme ne prévoit ni délai maximal
pour l’ensemble du téléversement, ni boucle de nouvelle tentative automatique ; définissez un délai
maximal distinct pour la tâche si votre service en a besoin.
Bonnes pratiques pour le téléversement de vidéos
Pour vérifier le contenu stocké, téléchargez le fichier source original et comparez chaque octet
avec votre fichier d’entrée. Les liens de fichiers de l’API de Vimeo
nécessitent les portées public, private et
video_files pour le jeton, ainsi qu’un forfait éligible pour le compte.
La disponibilité de l’original dépend également du
stockage des fichiers sources, qui nécessite un forfait payant et
doit être activé avant le téléversement. Une version transcodée est un fichier différent et ne
permet pas d’établir une correspondance avec les octets de l’original. Conservez votre propre
original et supprimez la vidéo de test privée de votre compte Vimeo une fois les tests terminés.
Améliorer les flux de travail avec le Robot d’export Vimeo de Transloadit
Utilisez 🤖 /video/encode pour l’encodage et l’ajout de filigranes, puis transmettez sa sortie à 🤖 /vimeo/store pour l’export. Le Robot d’export n’effectue pas lui-même toutes les transformations vidéo précédentes.
Conclusion
Conservez la session enregistrée jusqu’à ce que vous ayez vérifié la vidéo reçue et terminé toute procédure de récupération. Pour un traitement géré avant l’export, consultez le service d’encodage vidéo de Transloadit.
