Expo React-Native Youtube 视频上传使用 Fetch()
Expo React-Native Youtube video upload using 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 帐户后,我遇到了同样的问题。
Youtube 的文档也不是很清楚。 This 问题与你的问题类似,但是这个人使用了 axios。
他说他花了几个月的时间,但没弄清楚。之后他使用了 npm 中的 youtube-video-api
,https://www.npmjs.com/package/youtube-video-api.
我认为您应该转向其他解决方案,不要拘泥于此代码。
喜欢你可以托管一个node.js后端,然后将你的视频和视频信息发送到后端上传到youtube供应用用户使用。
我解决了!我从 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
解决了这个问题。我更新了上面的代码。
我正在尝试使用 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 帐户后,我遇到了同样的问题。
Youtube 的文档也不是很清楚。 This 问题与你的问题类似,但是这个人使用了 axios。
他说他花了几个月的时间,但没弄清楚。之后他使用了 npm 中的 youtube-video-api
,https://www.npmjs.com/package/youtube-video-api.
我认为您应该转向其他解决方案,不要拘泥于此代码。
喜欢你可以托管一个node.js后端,然后将你的视频和视频信息发送到后端上传到youtube供应用用户使用。
我解决了!我从 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
解决了这个问题。我更新了上面的代码。