上传录制文件

最近更新时间:2024-07-08 11:48:52

我的收藏

接口描述

描述:
企业 secret 鉴权用户可上传文件到录制文件列表,不支持 OAuth。
一次性上传不大于20MB的文件,接口限频200次/分钟。如果需要上传大于 20M 文件请使用分块上传。
录制文件上传结果可通过订阅通用 webhook 事件 异步任务结果 获取。录制文件上传场景的 business_code 值为:record.file-upload请参考下面示例中具体键值对的映射含义。
HTTP 的头部 Content-Type 必须传入 multipart/form-data。
该接口的输入参数是表单数据,不作为签名的部分。
注意:
本接口为限时免费接口。
请求方式:POST
鉴权方式:JWT
接口请求域名:
https://api.meeting.qq.com/v1/files/records/upload-all


输入参数

以下请求参数列表仅列出了接口请求参数,HTTP 请求头公共参数请参见签名验证章节的 公共参数说明
参数名称
是否必须
参数类型
参数描述
operator_id
String
操作者 ID。operator_id 必须与 operator_id_type 配合使用。根据 operator_id_type 的值,operator_id 代表不同类型。
operator_id_type
Integer
操作人 ID 类型:
1:userid
file_name
String
文件名+后缀,最多支持30个字符,base64编码。
file_type
String
文件类型,支持以下枚举值:
voice:音频,支持上传 m4a、aiff、wma、ogg、aac、amr、wav、mp3 格式。
video:视频,支持上传 mp4、flv、mov、ogg、avi、wmv、m4v、3gp、mpeg 格式。
file_size
Integer
文件大小(以字节为单位,最大20,971,520),需确保和实际文件大小相等。
file_checksum
String
文件校验和,文件内容 MD5 结果的十六进制表示。
file_content
[ ]Byte
文件二进制内容。
speak_number
Integer
上传文件中的发言人数:传具体数值代表几人发言,最多支持12人,其中0代表多人发言。
ai_record
Boolean
自动生成智能转写和智能纪要: true:自动生成(默认) false:不生成

输出参数

参数名称
参数类型
参数描述
job_id
String
任务 ID。

示例

输入示例

Java
Python
Go
Nodejs
cURL
import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Objects; import java.util.Random; import java.util.Base64; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.File; import com.alibaba.fastjson.JSONObject; import org.apache.commons.io.FileUtils; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ContentType; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.apache.commons.codec.digest.DigestUtils; public class UploadAllRecordFileExample { private static final String yourAppId = "yourAppId"; private static final String yourSdkId = "yourSdkId"; private static final String yourSecretId = "yourSecretId"; private static final String yourSecretKey = "yourSecretKey"; private static final String prodHost = "https://api.meeting.qq.com"; private static final String uploadRecordFileAllPath = "/v1/files/records/upload-all"; private static final String uploadRecordFileAllMethod = "POST"; public static void main(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeyException { byte[] fileContent = FileUtils.readFileToByteArray(new File("your_file_path")); String jobId = uploadAllRecordFile("operatorId", 1, "fileName", "video", fileContent, 1, true); if (!Objects.equals(jobId, "")) { System.out.println("Job ID: " + jobId); } } public static String uploadAllRecordFile(String operatorId, int operatorIdType, String fileName, String fileType, byte[] fileContent, int speakNumber, boolean aiRecord) throws IOException, NoSuchAlgorithmException, InvalidKeyException { HttpPost req = createUploadAllRecordFileRequest(operatorId, operatorIdType, fileName, fileType, fileContent, speakNumber, aiRecord); CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse resp = httpClient.execute(req); try { if (resp.getStatusLine().getStatusCode() != 200) { System.out.println("resp.status: " + resp.getStatusLine().getStatusCode()); System.out.println("resp.headers: " + Arrays.toString(resp.getAllHeaders())); System.out.println("resp.body: " + EntityUtils.toString(resp.getEntity())); return ""; } System.out.println("resp.headers: " + Arrays.toString(resp.getAllHeaders())); String respBody = EntityUtils.toString(resp.getEntity()); System.out.println("resp.body: " + respBody); JSONObject rspEntity = JSONObject.parseObject(respBody); return rspEntity.getString("job_id"); } finally { resp.close(); } } private static HttpPost createUploadAllRecordFileRequest(String operatorId, int operatorIdType, String fileName, String fileType, byte[] fileContent, int speakNumber, boolean aiRecord) throws NoSuchAlgorithmException, InvalidKeyException { String fileChecksum = DigestUtils.md5Hex(fileContent); MultipartEntityBuilder builder = MultipartEntityBuilder.create(); builder.addTextBody("operator_id", operatorId); builder.addTextBody("operator_id_type", Integer.toString(operatorIdType)); builder.addTextBody("file_name", Base64.getEncoder().encodeToString(fileName.getBytes(StandardCharsets.UTF_8))); builder.addTextBody("file_type", fileType); builder.addTextBody("file_size", Integer.toString(fileContent.length)); builder.addTextBody("file_checksum", fileChecksum); builder.addTextBody("speak_number", Integer.toString(speakNumber)); builder.addTextBody("ai_record", Boolean.toString(aiRecord)); builder.addBinaryBody("file_content", new ByteArrayInputStream(fileContent), ContentType.DEFAULT_BINARY, fileName); HttpEntity httpEntity = builder.build(); HttpPost req = new HttpPost(prodHost + uploadRecordFileAllPath); req.setEntity(httpEntity); String nonce = Integer.toString(new Random().nextInt(99999999) + 1); String timestamp = Long.toString(System.currentTimeMillis() / 1000); req.setHeader("Content-Type", httpEntity.getContentType().getValue()); req.setHeader("AppId", yourAppId); req.setHeader("SdkId", yourSdkId); req.setHeader("X-TC-Key", yourSecretId); req.setHeader("X-TC-Nonce", nonce); req.setHeader("X-TC-Timestamp", timestamp); req.setHeader("X-TC-Registered", "1"); req.setHeader("X-TC-Signature", generateSignature(uploadRecordFileAllMethod, nonce, timestamp, uploadRecordFileAllPath, "")); return req; } private static String generateSignature(String method, String nonce, String timestamp, String requestUri, String requestBody) throws NoSuchAlgorithmException, InvalidKeyException { // 实现签名逻辑 String headSignStr = String.format("X-TC-Key=%s&X-TC-Nonce=%s&X-TC-Timestamp=%s", yourSecretId, nonce, timestamp); String signStr = String.format("%s\\n%s\\n%s\\n%s", method, headSignStr, requestUri, requestBody); Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKeySpec = new SecretKeySpec(yourSecretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); mac.init(secretKeySpec); byte[] hash = mac.doFinal(signStr.getBytes(StandardCharsets.UTF_8)); String sha = bytesToHex(hash); return Base64.getEncoder().encodeToString(sha.getBytes(StandardCharsets.UTF_8)); } private static String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } }

import base64 import hashlib import hmac import json import random import time import requests from requests_toolbelt import MultipartEncoder your_app_id = "yourAppId" # 企业ID your_sdk_id = "yourSdkId" # 应用ID your_secret_id = "yourSecretId" # 应用密钥ID your_secret_key = "yourSecretKey" # 应用密钥KEY prod_host = "https://api.meeting.qq.com" upload_record_file_all_path = "/v1/files/records/upload-all" upload_record_file_all_method = "POST" # upload_all_record_file 上传文件到用户的录制文件列表 def upload_all_record_file(operator_id, operator_id_type, file_name, file_type, file_content, speak_number, ai_record): # 1、Prepare the request req = create_upload_all_record_file_request(operator_id, operator_id_type, file_name, file_type, file_content, speak_number, ai_record) # 2、Send the request resp = requests.request(req.method, req.url, data=req.data, headers=req.headers) # 3、Parse the response if resp.status_code != 200: print("resp.status:{status_code}\\n".format(status_code=resp.status_code)) print("resp.headers:", resp.headers, "\\n") print("resp.body:", resp.text, "\\n") return "" else: print("resp.headers:", resp.headers, "\\n") print("resp.body:", resp.text, "\\n") job_id = json.loads(resp.text).get('job_id') return job_id def create_upload_all_record_file_request(operator_id: str, operator_id_type: int, file_name: str, file_type: str, file_content: bytes, speak_number: int, ai_record: bool): multipart_data = MultipartEncoder( fields={ "operator_id": operator_id, "operator_id_type": str(operator_id_type), "file_name": base64.b64encode(file_name.encode()).decode(), "file_type": file_type, "file_size": str(len(file_content)), "file_checksum": hashlib.md5(file_content).hexdigest(), "speak_number": str(speak_number), "ai_record": str(ai_record).lower(), 'file_content': (file_name, file_content, "application/octet-stream") } ) random.seed(time.time()) nonce = random.randint(1, 100000000) timestamp = int(time.time()) url = prod_host + upload_record_file_all_path headers = { "Content-Type": multipart_data.content_type, "AppId": your_app_id, "SdkId": your_sdk_id, "X-TC-Key": your_secret_id, "X-TC-Nonce": str(nonce), "X-TC-Timestamp": str(timestamp), "X-TC-Registered": "1", } # gen the signature signature = gen_jwt_signature(upload_record_file_all_method, str(nonce), str(timestamp), upload_record_file_all_path, "") headers["X-TC-Signature"] = signature return requests.Request(upload_record_file_all_method, url, data=multipart_data, headers=headers) def gen_jwt_signature(method: str, nonce: str, timestamp: str, request_uri: str, request_body: str): head_sign_str = "{0}\\nX-TC-Key={1}&X-TC-Nonce={2}&X-TC-Timestamp={3}\\n{4}\\n{5}".format( method, your_secret_id, nonce, timestamp, request_uri, request_body) signature = hmac.new(your_secret_key.encode(), head_sign_str.encode(), hashlib.sha256).hexdigest() return base64.b64encode(signature.encode()).decode()

import ( "bytes" "context" "crypto/hmac" "crypto/md5" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "math/rand" "mime/multipart" "net/http" "strconv" "time" ) const ( yourAppId = "yourAppId" // 企业ID yourSdkId = "yourSdkId" // 应用ID yourSecretId = "yourSecretId" // 应用密钥ID yourSecretKey = "yourSecretKey" // 应用密钥KEY prodHost = "https://api.meeting.qq.com" uploadRecordFileAllPath = "/v1/files/records/upload-all" uploadRecordFileAllMethod = "POST" ) // UploadAllRecordFile 上传文件到用户的录制文件列表 // @description 一次性上传不大于20MB的文件,接口限频200次/分钟。如果需要上传大于 20M 文件请使用分块上传。 /** * @param operatorId 操作者ID; * @param operatorIdType 操作人ID类型; * @param fileName 文件名+后缀,最多支持30个字符,base64编码; * @param fileType 文件类型,支持以下枚举值:1、voice:音频,支持上传m4a、aiff、wma、ogg、aac、amr、wav、mp3格式;2、video:视频,支持上传mp4、flv、mov、ogg、avi、wmv、m4v、3gp、mpeg格式; * @param fileContent 文件二进制内容; * @param speakNumber 上传文件中的发言人数:传具体数值代表几人发言,最多支持12人,其中0代表多人发言; * @param aiRecord 自动生成智能转写和智能纪要:true:自动生成(默认),false:不生成; * @return jobId 上传录制文件的任务ID; * @return err 错误原因; */ func UploadAllRecordFile(ctx context.Context, operatorId string, operatorIdType int, fileName, fileType string, fileContent []byte, speakNumber int, aiRecord bool) (jobId string, err error) { // 1、Prepare the request req := createUploadAllRecordFileRequest(ctx, operatorId, operatorIdType, fileName, fileType, fileContent, speakNumber, aiRecord) // 2、Send the request resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println(err) return } defer resp.Body.Close() // 3、Parse the response respEntity := &UploadAllRecordFileRsp{} err = parseResponse(resp, respEntity) if err != nil { fmt.Println(err) return } return respEntity.JobId, nil } func createUploadAllRecordFileRequest(ctx context.Context, operatorId string, operatorIdType int, fileName, fileType string, fileContent []byte, speakNumber int, aiRecord bool) *http.Request { fileSize := len(fileContent) md5Content := md5.Sum(fileContent) fileCheckSum := hex.EncodeToString(md5Content[:]) bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf) _ = bodyWriter.WriteField("operator_id", operatorId) _ = bodyWriter.WriteField("operator_id_type", strconv.Itoa(operatorIdType)) _ = bodyWriter.WriteField("file_name", base64.StdEncoding.EncodeToString([]byte(fileName))) _ = bodyWriter.WriteField("file_type", fileType) _ = bodyWriter.WriteField("file_size", strconv.Itoa(fileSize)) _ = bodyWriter.WriteField("file_checksum", fileCheckSum) _ = bodyWriter.WriteField("speak_number", strconv.Itoa(speakNumber)) _ = bodyWriter.WriteField("ai_record", strconv.FormatBool(aiRecord)) formFile, _ := bodyWriter.CreateFormFile("file_content", fileName) _, _ = io.Copy(formFile, bytes.NewReader(fileContent)) _ = bodyWriter.Close() rand.Seed(time.Now().UnixNano()) nonce := rand.Intn(100000000) + 1 timestamp := time.Now().Unix() req, _ := http.NewRequest(uploadRecordFileAllMethod, prodHost+uploadRecordFileAllPath, bodyBuf) req.Header.Set("Content-Type", bodyWriter.FormDataContentType()) req.Header.Set("AppId", yourAppId) req.Header.Set("SdkId", yourSdkId) req.Header.Set("X-TC-Key", yourSecretId) req.Header.Set("X-TC-Nonce", fmt.Sprintf("%d", nonce)) req.Header.Set("X-TC-Timestamp", fmt.Sprintf("%d", timestamp)) req.Header.Set("X-TC-Registered", "1") // gen the signature signature := GenJwtSignature(ctx, uploadRecordFileAllMethod, strconv.Itoa(nonce), strconv.Itoa(int(timestamp)), uploadRecordFileAllPath, "") req.Header.Set("X-TC-Signature", signature) return req } // UploadAllRecordFileRsp 上传录制文件响应 type UploadAllRecordFileRsp struct { JobId string `json:"job_id"` // 任务ID } func parseResponse(resp *http.Response, respEntity interface{}) (err error) { respBody, err := io.ReadAll(resp.Body) if err != nil { return err } fmt.Printf("resp status: %s\\n", resp.Status) fmt.Println("resp headers:") for k, v := range resp.Header { fmt.Printf("%s: %s\\n", k, v) } fmt.Printf("respBody: %s\\n", string(respBody)) if err = json.Unmarshal(respBody, respEntity); err != nil { return err } fmt.Printf("resp entity: %+v\\n", respEntity) return nil } // GenJwtSignature 生成签名 /** * @param httpMethod http请求方法 GET/POST/PUT等 * @param headerNonce X-TC-Nonce请求头,随机数 * @param headerTimestamp X-TC-Timestamp请求头,当前时间的秒级时间戳 * @param requestUri 请求uri,eg:/v1/meetings * @param requestBody 请求体,没有的设为空串 * @return 签名,需要设置在请求头X-TC-Signature中 */ func GenJwtSignature(ctx context.Context, method string, nonce string, timestamp string, requestUri string, requestBody string) string { headSignStr := fmt.Sprintf("X-TC-Key=%s&X-TC-Nonce=%s&X-TC-Timestamp=%s", yourSecretId, nonce, timestamp) signStr := fmt.Sprintf("%s\\n%s\\n%s\\n%s", method, headSignStr, requestUri, requestBody) h := hmac.New(sha256.New, []byte(yourSecretKey)) h.Write([]byte(signStr)) sha := hex.EncodeToString(h.Sum([]byte{})) signBase64 := base64.StdEncoding.EncodeToString([]byte(sha)) return signBase64 }

const crypto = require('crypto'); const fs = require('fs'); const axios = require('axios'); // npm install axios const FormData = require('form-data'); // npm install form-data const yourAppId = 'yourAppId'; const yourSdkId = 'yourSdkId'; const yourSecretId = 'yourSecretId'; const yourSecretKey = 'yourSecretKey'; const prodHost = 'https://api.meeting.qq.com'; const uploadRecordFileAllPath = '/v1/files/records/upload-all'; const uploadRecordFileAllMethod = 'POST'; function readFileBytes(path) { return fs.readFileSync(path); } async function uploadAllRecordFile(operatorId, operatorIdType, fileName, fileType, fileContent, speakNumber, aiRecord) { const req = createUploadAllRecordFileRequest(operatorId, operatorIdType, fileName, fileType, fileContent, speakNumber, aiRecord); const resp = await axios(req); if (resp.status !== 200) { console.log('resp.status:', resp.status); console.log('resp.headers:', resp.headers); console.log('resp.body:', resp.data); return ''; } else { console.log('resp.headers:', resp.headers); console.log('resp.body:', resp.data); return resp.data.job_id; } } function createUploadAllRecordFileRequest(operatorId, operatorIdType, fileName, fileType, fileContent, speakNumber, aiRecord) { const formData = new FormData(); formData.append('operator_id', operatorId); formData.append('operator_id_type', operatorIdType); formData.append('file_name', Buffer.from(fileName).toString('base64')); formData.append('file_type', fileType); formData.append('file_size', fileContent.length); formData.append('file_checksum', crypto.createHash('md5').update(fileContent).digest('hex')); formData.append('speak_number', speakNumber); formData.append('ai_record', aiRecord.toString().toLowerCase()); formData.append('file_content', fileContent, {filename: fileName}); const nonce = Math.floor(Math.random() * 100000000) + 1; const timestamp = Math.floor(Date.now() / 1000); const signature = genJwtSignature(uploadRecordFileAllMethod, nonce.toString(), timestamp.toString(), uploadRecordFileAllPath, ''); const headers = { ...formData.getHeaders(), 'AppId': yourAppId, 'SdkId': yourSdkId, 'X-TC-Key': yourSecretId, 'X-TC-Nonce': nonce, 'X-TC-Timestamp': timestamp, 'X-TC-Registered': '1', 'X-TC-Signature': signature }; return { method: uploadRecordFileAllMethod, url: prodHost + uploadRecordFileAllPath, headers: headers, data: formData }; } function genJwtSignature(method, nonce, timestamp, requestUri, requestBody) { const headSignStr = `${method}\\nX-TC-Key=${yourSecretId}&X-TC-Nonce=${nonce}&X-TC-Timestamp=${timestamp}\\n${requestUri}\\n${requestBody}`; const signature = crypto.createHmac('sha256', yourSecretKey).update(headSignStr).digest('hex'); return Buffer.from(signature).toString('base64'); } (async () => { const fileContent = readFileBytes('your_file_path'); const jobId = await uploadAllRecordFile('your_operator_id', 1, 'your_file_name', 'video', fileContent, 1, true); console.log(jobId); })();

curl --location 'http://api.meeting.qq.com/v1/files/records/upload-all' \\
--header 'Content-Type: multipart/form-data' \\
--header 'SdkId: xxxxxx' \\
--header 'AppId: xxxxxx' \\
--header 'X-TC-Key: hNYX1K5MxxxxxxxxxxxrHh9aGkumq' \\
--header 'X-TC-Timestamp: 1716954227' \\
--header 'X-TC-Nonce: xxxxxx' \\
--header 'X-TC-Signature: xxxxxx' \\
--form 'operator_id_type="1"' \\
--form 'operator_id="xxxxxx"' \\
--form 'file_name="c3VuZxxxxx1wNA=="' \\
--form 'file_type="video"' \\
--form 'file_size="1795792"' \\
--form 'file_checksum="eab41e228e4xxxxxxxxxxeaa6ce20"' \\
--form 'speak_number="1"' \\
--form 'ai_record="true"' \\
--form 'file_content=@"xxxx.mp4"'

输出示例

{
"job_id": "QNLLdhFLAJh9azYd6_xxxxxxxxxP6VbZMdqNiyltVM"
}

webhook 示例

本接口采用公共事件 异步任务结果,当 business_code 值为 record.file-upload 时,对应键值对的映射含义:
{
"event": "common.job-results",
"trace_id": "e7aa65dd-f7e6-4b62-912c-2035173b34a9",
"payload": [
{
"operate_time": 1609313201465,
"business_code": "record.file-upload", // 代表录制文件上传场景唯一标识,用于区分不同场景下返回的键值对含义
"job_id": "xxxxxxxxxx", // 与异步操作关联的唯一id
"job_status": 1, // 异步操作结果 1 成功 2 失败
"error_msg": "", // 异步操作失败的错误信息
"notify_info": { // 推送具体的任务信息
"success": [
[
{
"key": "file_name",
"value": "xxxxx"
},
{
"key": "meeting_record_id",
"value": "xxxx"
},
{
"key": "record_file_id",
"value": "xxxx"
}
]
],
"failed": [
[
{
"key": "error_code",
"value": "xxxx"
},
{
"key": "error_msg",
"value": "xxxx"
},
{
"key": "file_name",
"value": "xxxx"
},
{
"key": "meeting_record_id", //部分失败场景不会返回该键值对
"value": "xxxx"
},
{
"key": "record_file_id", //部分失败场景不会返回该键值对
"value": "xxxx"
}
]
]
}
}
]
}