Skip to content

管理云录像

业务服务端用 HTTP API 查询录像文件索引,或删除指定设备在一段时间内的云端录像。这些接口需要应用级 AccessKey 签名,由业务服务端调用。

准备签名请求函数

查询和删除复用云端签发 Token里的 signTGV1。Method、Path、Query 和 Body 必须是当前请求最终发送的值。

查询录像文件索引

按设备和 UTC 毫秒时间范围查询。单次查询范围最长 31 天。has_moretrue 时,把返回的 next_page_token 填入下一页的 page_token,设备和时间范围与第一页相同。has_morefalse 时结束分页。

响应里的时间戳和字节数是十进制字符串,用 string 接收。

go
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 结果。

pathsize_bytes 识别文件。sha256 是设备上报时可能附带的摘要。

每一页是发出该次请求时的查询结果。分页过程中如果有新的录像索引进入同一时间范围,后续页面可能与已读页面重复或漏项。要得到一份稳定清单,等该时间范围不再写入新索引后,再从第一页查起。

删除云端录像

删除范围使用左闭右开的 UTC 毫秒区间 [start_time_ms, end_time_ms)。响应里的起止时间与这次请求提交的范围一致。发送请求前确认当前操作者有权删除目标设备的录像。

客户端 SDK 没有删除云录像的接口。RecordingFile 或 SnapshotFile 的 delete() 只删除客户端本地生成的 MP4 或 JPEG,不会影响云端录像。

go
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
}

删除请求以 AppIddevice_idstart_time_msend_time_ms 的完整组合判断是否重复。第一次写入这组精确参数时 effectivetrue,再次请求返回 false。两个范围只要起止时间不同,就会各写一条记录。

删除生效后,客户端查询和播放会扣除对应时间范围。用客户端查询确认删除结果。文件索引按物理文件完整覆盖才隐藏该项;存储空间随后回收。

完整参数、响应字段和错误码见 HTTP API删除录像

TiStore 开发文档