@@ -868,6 +868,89 @@ def __str__(self):
868868
869869 return json .dumps (json_str , indent = 2 )
870870
871+ def upload_video (
872+ self ,
873+ video_path : str ,
874+ * ,
875+ batch_name : Optional [str ] = None ,
876+ tag_names : Optional [Union [str , List [str ]]] = None ,
877+ metadata : Optional [Dict ] = None ,
878+ split : Optional [str ] = None ,
879+ wait : bool = False ,
880+ poll_interval : float = 2 ,
881+ poll_timeout : float = 300 ,
882+ ) -> Dict :
883+ """Upload original MP4/MOV bytes as a native video Source.
884+
885+ Returns the API processing status, including ``videoId``. Once the
886+ status is ``uploaded``, that ID is the canonical Source ID to annotate.
887+ The ID can change during ingestion if the video is deduplicated.
888+ ``wait=False`` reads status once after the signed PUT; use
889+ :meth:`wait_for_video_upload` to continue polling later.
890+ """
891+ if not os .path .isfile (video_path ):
892+ raise ValueError (f"Video file not found: { video_path } " )
893+ content_type = {".mp4" : "video/mp4" , ".mov" : "video/quicktime" }.get (os .path .splitext (video_path )[1 ].lower ())
894+ if content_type is None :
895+ raise ValueError ("Native video upload accepts .mp4 and .mov files" )
896+
897+ body : Dict = {"name" : os .path .basename (video_path ), "contentType" : content_type }
898+ if batch_name is not None :
899+ body ["batch" ] = batch_name
900+ if tag_names is not None :
901+ body ["tag" ] = tag_names
902+ if metadata is not None :
903+ body ["metadata" ] = metadata
904+ if split is not None :
905+ body ["split" ] = split
906+
907+ prepared = rfapi .prepare_video_upload (self .__api_key , self .__workspace , self .__project_name , body )
908+ video_id = prepared ["videoId" ]
909+ if not prepared .get ("signedUrl" ):
910+ raise rfapi .RoboflowError ("Video upload API did not return a signedUrl" )
911+ rfapi .put_video_upload (prepared ["signedUrl" ], video_path , prepared .get ("requiredHeaders" , {}), content_type )
912+ if wait :
913+ return self .wait_for_video_upload (video_id , poll_interval = poll_interval , poll_timeout = poll_timeout )
914+ return self .get_video_upload_status (video_id )
915+
916+ def get_video_upload_status (self , video_id : str , * , timeout : Optional [float ] = None ) -> Dict :
917+ """Get a native video's processing state and canonical Source ID.
918+
919+ ``timeout`` limits connection and response-read inactivity. It is not
920+ a strict total request-duration cap.
921+ """
922+ return rfapi .get_video_upload_status (self .__api_key , self .__workspace , video_id , timeout = timeout )
923+
924+ def wait_for_video_upload (self , video_id : str , * , poll_interval : float = 2 , poll_timeout : float = 300 ) -> Dict :
925+ """Poll until uploaded or failed, limiting each status read to the remaining budget.
926+
927+ With ``poll_timeout=0``, perform one status read using the default
928+ transport timeout and return a terminal result if it is already ready.
929+ Requests' timeouts measure connection/read inactivity, so this is not
930+ a strict wall-clock cap on a slowly streaming response.
931+ """
932+ if poll_interval <= 0 or poll_timeout < 0 :
933+ raise ValueError ("poll_interval must be positive and poll_timeout must be nonnegative" )
934+ deadline = time .monotonic () + poll_timeout
935+ while True :
936+ remaining = deadline - time .monotonic ()
937+ if poll_timeout > 0 and remaining <= 0 :
938+ raise rfapi .RoboflowError (
939+ f"Video upload { video_id } did not finish within the { poll_timeout } s polling budget; "
940+ "call get_video_upload_status to check later"
941+ )
942+ request_timeout = min (rfapi .VIDEO_UPLOAD_STATUS_TIMEOUT , remaining ) if poll_timeout > 0 else None
943+ status = self .get_video_upload_status (video_id , timeout = request_timeout )
944+ if status .get ("status" ) in {"uploaded" , "failed" }:
945+ return status
946+ remaining = deadline - time .monotonic ()
947+ if remaining <= 0 :
948+ raise rfapi .RoboflowError (
949+ f"Video upload { video_id } is still { status .get ('status' )} after { poll_timeout } s; "
950+ "call get_video_upload_status to check later"
951+ )
952+ time .sleep (min (poll_interval , remaining ))
953+
871954 def image (self , image_id : str ) -> Dict :
872955 """
873956 Fetch the details of a specific image from the Roboflow API.
0 commit comments