管理云录像
业务服务端用 HTTP API 查询录像文件索引,或删除指定设备在一段时间内的云端录像。这些接口需要应用级 AccessKey 签名,由业务服务端调用。
准备签名请求函数
查询和删除复用云端签发 Token里的 signTGV1。Method、Path、Query 和 Body 必须是当前请求最终发送的值。
查询录像文件索引
按设备和 UTC 毫秒时间范围查询。单次查询范围最长 31 天。has_more 为 true 时,把返回的 next_page_token 填入下一页的 page_token,设备和时间范围与第一页相同。has_more 为 false 时结束分页。
响应里的时间戳和字节数是十进制字符串,用 string 接收。
type recordingFile struct {
Path string `json:"path"`
SizeBytes string `json:"size_bytes"`
SHA256 string `json:"sha256"`
}
type recordingFileIndex struct {
OSSID string `json:"oss_id"`
RecordingRetentionDays int `json:"recording_retention_days"`
StartTimeMs string `json:"start_time_ms"`
EndTimeMs string `json:"end_time_ms"`
TotalSizeBytes string `json:"total_size_bytes"`
Files []recordingFile `json:"files"`
}
func queryRecordingFileIndexes(deviceID string, startTimeMs, endTimeMs int64) ([]recordingFileIndex, error) {
var items []recordingFileIndex
pageToken := ""
for {
query := url.Values{}
query.Set("device_id", deviceID)
query.Set("start_time_ms", strconv.FormatInt(startTimeMs, 10))
query.Set("end_time_ms", strconv.FormatInt(endTimeMs, 10))
query.Set("page_size", "100")
if pageToken != "" {
query.Set("page_token", pageToken)
}
rawQuery := query.Encode()
header, err := signTGV1(
http.MethodGet,
"/openapi/v1/query-recording-file-indexes",
rawQuery,
nil,
os.Getenv("TISTORE_APP_ID"),
os.Getenv("TISTORE_ACCESS_KEY_ID"),
os.Getenv("TISTORE_ACCESS_KEY_SECRET"),
time.Now().UTC(),
)
if err != nil {
return nil, err
}
req, err := http.NewRequest(
http.MethodGet,
"https://api-tistore.tange365.com/openapi/v1/query-recording-file-indexes?"+rawQuery,
nil,
)
if err != nil {
return nil, err
}
req.Header = header.Clone()
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
var envelope struct {
Code string `json:"code"`
RequestID string `json:"request_id"`
Data struct {
Items []recordingFileIndex `json:"items"`
HasMore bool `json:"has_more"`
NextPageToken string `json:"next_page_token"`
} `json:"data"`
}
err = json.NewDecoder(resp.Body).Decode(&envelope)
resp.Body.Close()
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK || envelope.Code != "OK" {
return nil, fmt.Errorf("TiStore request failed: status=%d code=%s request_id=%s", resp.StatusCode, envelope.Code, envelope.RequestID)
}
items = append(items, envelope.Data.Items...)
if !envelope.Data.HasMore || envelope.Data.NextPageToken == "" {
break
}
pageToken = envelope.Data.NextPageToken
}
return items, nil
}录像文件索引描述云端保存的物理文件。播放使用客户端 SDK 的 listRecordings 结果。
用 path 和 size_bytes 识别文件。sha256 是设备上报时可能附带的摘要。
每一页是发出该次请求时的查询结果。分页过程中如果有新的录像索引进入同一时间范围,后续页面可能与已读页面重复或漏项。要得到一份稳定清单,等该时间范围不再写入新索引后,再从第一页查起。
删除云端录像
删除范围使用左闭右开的 UTC 毫秒区间 [start_time_ms, end_time_ms)。响应里的起止时间与这次请求提交的范围一致。发送请求前确认当前操作者有权删除目标设备的录像。
客户端 SDK 没有删除云录像的接口。RecordingFile 或 SnapshotFile 的 delete() 只删除客户端本地生成的 MP4 或 JPEG,不会影响云端录像。
func deleteRecordings(deviceID string, startTimeMs, endTimeMs int64) (bool, error) {
body, err := json.Marshal(struct {
DeviceID string `json:"device_id"`
StartTimeMs int64 `json:"start_time_ms"`
EndTimeMs int64 `json:"end_time_ms"`
}{
DeviceID: deviceID,
StartTimeMs: startTimeMs,
EndTimeMs: endTimeMs,
})
if err != nil {
return false, err
}
header, err := signTGV1(
http.MethodPost,
"/openapi/v1/delete-recordings",
"",
body,
os.Getenv("TISTORE_APP_ID"),
os.Getenv("TISTORE_ACCESS_KEY_ID"),
os.Getenv("TISTORE_ACCESS_KEY_SECRET"),
time.Now().UTC(),
)
if err != nil {
return false, err
}
req, err := http.NewRequest(
http.MethodPost,
"https://api-tistore.tange365.com/openapi/v1/delete-recordings",
bytes.NewReader(body),
)
if err != nil {
return false, err
}
req.Header = header.Clone()
req.ContentLength = int64(len(body))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
var envelope struct {
Code string `json:"code"`
RequestID string `json:"request_id"`
Data struct {
StartTimeMs string `json:"start_time_ms"`
EndTimeMs string `json:"end_time_ms"`
Effective bool `json:"effective"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&envelope); err != nil {
return false, err
}
if resp.StatusCode != http.StatusOK || envelope.Code != "OK" {
return false, fmt.Errorf("TiStore request failed: status=%d code=%s request_id=%s", resp.StatusCode, envelope.Code, envelope.RequestID)
}
return envelope.Data.Effective, nil
}删除请求以 AppId、device_id、start_time_ms 和 end_time_ms 的完整组合判断是否重复。第一次写入这组精确参数时 effective 为 true,再次请求返回 false。两个范围只要起止时间不同,就会各写一条记录。
删除生效后,客户端查询和播放会扣除对应时间范围。用客户端查询确认删除结果。文件索引按物理文件完整覆盖才隐藏该项;存储空间随后回收。