react-native - Expo React-Native Youtube 视频上传使用 Fetch()
问题描述
我正在尝试使用 V3 Youtube.video.insert
API 方法在 youtube 上上传视频。当我调用该方法时,我收到以下错误消息:Bad request: Request contains an invalid argument.
. 尽管出现错误消息,我的上传内容仍会出现在我的个人 YouTube 帐户中的“我的视频”下。我是 React Native 的新手,我很难理解 Youtube API 文档,有人可以向我解释我做错了什么或者我该如何解决?
这是我目前的要求:
let response = await fetch(
'https://youtube.googleapis.com/youtube/v3/videos?key=' + API_KEY,
{
method: 'POST',
headers: {
'Authorization': 'Bearer ' + accessToken,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
part: 'id,snippet,status',
notifySubscribers: false,
requestBody: {
snippet: {
title: 'YouTube Upload Test',
description: 'Testing YouTube upload',
},
status: {
privacyStatus: 'private',
},
},
media: {
body: 'file:///data/user/0/host.exp.exponent/cache/ExperienceData/Camera/video.mp4',
}
})
}
);
我试着把所有东西都拿出来,body:
但我得到了同样的回应。
以下是我试图理解的链接: https://developers.google.com/youtube/v3/docs/videos/insert https://github.com/googleapis/google-api-nodejs-client/blob /master/samples/youtube/upload.js
更新:
好的,我想我知道了,但我仍然不知道如何附加视频文件......这是我现在的代码:
let response = await fetch(
'https://youtube.googleapis.com/youtube/v3/videos?part=snippet&part=status&key=' + API_KEY,
{
method: 'POST',
headers: {
'Authorization': 'Bearer ' + accessToken,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
snippet: {
title: "This is the title",
description: "This is the description.",
},
status: {
privacyStatus: 'private',
}
}),
}
);
解决方案
我解决了!我从 YouTube API 示例中获取了corse_upload.js,并在 React Native / Expo 中实现了它。
这是我正在使用的 React Native 中的API.js :
import React, { Component } from 'react';
export default class API extends Component {
constructor(props) {
super(props);
const obj = this;
const DRIVE_UPLOAD_URL = 'https://www.googleapis.com/upload/drive/v2/files/';
var options = props;
var noop = function() {};
this.file = options.file;
this.contentType = options.contentType || this.file.type || 'application/octet-stream';
this.metadata = options.metadata || {
'title': this.file.name,
'mimeType': this.contentType
};
this.token = options.token;
this.onComplete = options.onComplete || noop;
this.onProgress = options.onProgress || noop;
this.onError = options.onError || noop;
this.offset = options.offset || 0;
this.chunkSize = options.chunkSize || 0;
//this.retryHandler = new RetryHandler();
this.retryHandler = new obj.RetryHandler();
this.url = options.url;
if (!this.url) {
var params = options.params || {};
params.uploadType = 'resumable';
//this.url = this.buildUrl_(options.fileId, params, options.baseUrl);
this.url = obj.buildUrl_(options.fileId, params, options.baseUrl);
}
this.httpMethod = options.fileId ? 'PUT' : 'POST';
}
RetryHandler = function() {
this.interval = 1000; // Start at one second
this.maxInterval = 60 * 1000; // Don't wait longer than a minute
};
retry = function(fn) {
setTimeout(fn, this.interval);
this.interval = this.nextInterval_();
};
reset = function() {
this.interval = 1000;
};
nextInterval_ = function() {
var interval = this.interval * 2 + this.getRandomInt_(0, 1000);
return Math.min(interval, this.maxInterval);
};
getRandomInt_ = function(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
};
buildQuery_ = function(params) {
params = params || {};
return Object.keys(params).map(function(key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
}).join('&');
};
buildUrl_ = function(id, params, baseUrl) {
var url = baseUrl || DRIVE_UPLOAD_URL;
if (id) {
url += id;
}
var query = this.buildQuery_(params);
if (query) {
url += '?' + query;
}
return url;
};
upload = function() {
//var self = this;
var xhr = new XMLHttpRequest();
xhr.open(this.httpMethod, this.url, true);
xhr.setRequestHeader('Authorization', 'Bearer ' + this.token);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-Upload-Content-Length', this.file.size);
xhr.setRequestHeader('X-Upload-Content-Type', this.contentType);
xhr.onload = function(e) {
if (e.target.status < 400) {
var location = e.target.getResponseHeader('Location');
this.url = location;
this.sendFile_();
} else {
this.onUploadError_(e);
}
}.bind(this);
xhr.onerror = this.onUploadError_.bind(this);
xhr.send(JSON.stringify(this.metadata));
};
sendFile_ = function() {
var content = this.file;
console.log(content);
var end = this.file.size;
if (this.offset || this.chunkSize) {
// Only bother to slice the file if we're either resuming or uploading in chunks
if (this.chunkSize) {
end = Math.min(this.offset + this.chunkSize, this.file.size);
}
content = content.slice(this.offset, end);
console.log(content);
}
var xhr = new XMLHttpRequest();
xhr.open('PUT', this.url, true);
xhr.setRequestHeader('Content-Type', this.contentType);
xhr.setRequestHeader('Content-Range', 'bytes ' + this.offset + '-' + (end - 1) + '/' + this.file.size);
xhr.setRequestHeader('X-Upload-Content-Type', this.file.type);
if (xhr.upload) {
xhr.upload.addEventListener('progress', this.onProgress);
}
xhr.onload = this.onContentUploadSuccess_.bind(this);
xhr.onerror = this.onContentUploadError_.bind(this);
xhr.send(content);
console.log(content);
};
resume_ = function() {
var xhr = new XMLHttpRequest();
xhr.open('PUT', this.url, true);
xhr.setRequestHeader('Content-Range', 'bytes */' + this.file.size);
xhr.setRequestHeader('X-Upload-Content-Type', this.file.type);
if (xhr.upload) {
xhr.upload.addEventListener('progress', this.onProgress);
}
xhr.onload = this.onContentUploadSuccess_.bind(this);
xhr.onerror = this.onContentUploadError_.bind(this);
xhr.send();
};
extractRange_ = function(xhr) {
var range = xhr.getResponseHeader('Range');
if (range) {
this.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1;
}
};
onContentUploadSuccess_ = function(e) {
if (e.target.status == 200 || e.target.status == 201) {
this.onComplete(e.target.response);
} else if (e.target.status == 308) {
this.extractRange_(e.target);
this.reset();
this.sendFile_();
}
};
onContentUploadError_ = function(e) {
if (e.target.status && e.target.status < 500) {
this.onError(e.target.response);
} else {
this.retry(this.resume_.bind(this));
}
};
onUploadError_ = function(e) {
this.onError(e.target.response); // TODO - Retries for initial upload
};
}
这是我在App.js中使用它的方式:
import YTDAPI from './assets/API'
import RNFS from 'react-native-fs';
uploadMediaToYouTube = async function(accessToken, videoUri) {
const fileInfo = await RNFS.stat(videoUri);
var file = {
name: fileInfo.path.split('/').pop(),
size: fileInfo.size,
uri: fileInfo.path,
type: 'video/mp4'
}
var metadata = {
snippet: {
title: 'This is a new title',
description: 'This is a new description',
tags: ['youtube-cors-upload'],
categoryId: 22
},
status: {
privacyStatus: 'unlisted'
}
};
var uploader = new YTDAPI({
baseUrl: 'https://www.googleapis.com/upload/youtube/v3/videos',
file: file,
token: this.accessToken,
metadata: metadata,
params: {
part: Object.keys(metadata).join(',')
},
onError: function(data) {
console.log(data);
var message = data;
try {
var errorResponse = JSON.parse(data);
message = errorResponse.error.message;
} finally {
alert(message);
}
}.bind(this),
onProgress: function(data) {
var currentTime = Date.now();
var bytesUploaded = data.loaded;
var totalBytes = data.total;
var bytesPerSecond = bytesUploaded / ((currentTime - window.uploadStartTime) / 1000);
var estimatedSecondsRemaining = (totalBytes - bytesUploaded) / bytesPerSecond;
var percentageComplete = (bytesUploaded * 100) / totalBytes;
console.log("Uploaded: " + bytesUploaded + " | Total: " + totalBytes + " | Percentage: " + percentageComplete + " | Esitmated seconds remaining: " + estimatedSecondsRemaining);
}.bind(this),
onComplete: function(data) {
console.log("Complete");
}.bind(this)
});
window.uploadStartTime = Date.now();
uploader.upload();
}
为此,您需要在 Google Cloud Console 上配置 API 密钥、Web ClientId 和 oauth2 同意屏幕,并允许 Youtube Data V3 API 库并对用户进行身份验证以获得access_token
您将传递给我的函数的用户。
更新
使用 Fetch() 获取文件会导致应用程序因大文件而崩溃,因为它会尝试将整个视频文件加载到内存中。我通过使用解决了这个问题react-native-fs
。我在上面更新了我的代码。