@@ -29,13 +29,17 @@ import {
2929 LLMFunctionCallStartedData ,
3030 LLMFunctionCallStoppedData ,
3131 MediaState ,
32+ MimeTypeMapping ,
3233 Participant ,
3334 PipecatMetricsData ,
3435 RTVI_PROTOCOL_VERSION ,
3536 RTVIEvent ,
3637 RTVIEvents ,
38+ RTVIFile ,
39+ RTVIFileFormat ,
3740 RTVIMessage ,
3841 RTVIMessageType ,
42+ SendFileOptions ,
3943 SendTextOptions ,
4044 setAboutClient ,
4145 TranscriptData ,
@@ -1091,7 +1095,9 @@ export class PipecatClient extends RTVIEventEmitter {
10911095 public cancelUIJobGroup ( jobId : string , reason ?: string ) : void {
10921096 const payload : UICancelJobGroupData = { job_id : jobId } ;
10931097 if ( reason !== undefined ) payload . reason = reason ;
1094- this . _sendMessage ( new RTVIMessage ( RTVIMessageType . UI_CANCEL_JOB_GROUP , payload ) ) ;
1098+ this . _sendMessage (
1099+ new RTVIMessage ( RTVIMessageType . UI_CANCEL_JOB_GROUP , payload )
1100+ ) ;
10951101 }
10961102
10971103 /**
@@ -1155,6 +1161,187 @@ export class PipecatClient extends RTVIEventEmitter {
11551161 ) ;
11561162 }
11571163
1164+ @transportReady
1165+ public async sendFile (
1166+ file : RTVIFile | File ,
1167+ content : string ,
1168+ options : SendFileOptions = { }
1169+ ) {
1170+ if (
1171+ this . _botVersion [ 0 ] < 1 ||
1172+ ( this . _botVersion [ 0 ] === 1 && this . _botVersion [ 1 ] < 3 )
1173+ ) {
1174+ throw new RTVIErrors . UnsupportedFeatureError (
1175+ "sendFile" ,
1176+ "bot" ,
1177+ "requires RTVI protocol 2.2.0+"
1178+ ) ;
1179+ }
1180+ let rtvi_file = file instanceof File ? ( { } as RTVIFile ) : file ;
1181+ let mimeType : string =
1182+ file instanceof File ? file . type : rtvi_file . format . toLowerCase ( ) ;
1183+ if ( mimeType in MimeTypeMapping ) {
1184+ mimeType = MimeTypeMapping [ mimeType as RTVIFileFormat ] ;
1185+ }
1186+ rtvi_file . format = mimeType ;
1187+
1188+ const sendFileMessage = async ( ) => {
1189+ await this . _sendMessage (
1190+ new RTVIMessage ( RTVIMessageType . SEND_FILE , {
1191+ file : rtvi_file ,
1192+ content,
1193+ options,
1194+ } )
1195+ ) ;
1196+ } ;
1197+
1198+ let uploadFile : File | undefined ;
1199+ if ( file instanceof File ) {
1200+ // Estimate the message size with base64 encoding overhead (~33% larger)
1201+ // Add buffer for message wrapper overhead. This saves us from having to
1202+ // unnecessarily read the file into memory and encode it to base64.
1203+ const estimatedEncodedSize = Math . ceil ( file . size * 1.37 ) + 1000 ;
1204+
1205+ if ( estimatedEncodedSize > this . _transport . maxMessageSize ) {
1206+ uploadFile = file ;
1207+ } else {
1208+ return new Promise < void > ( ( resolve ) => {
1209+ const reader = new FileReader ( ) ;
1210+ reader . onload = async ( e ) => {
1211+ if ( ! e . target ?. result ) {
1212+ throw new RTVIErrors . RTVIError ( "Could not read file data" ) ;
1213+ }
1214+ const fileContent = e . target . result as string ;
1215+
1216+ rtvi_file = {
1217+ format : file . type ,
1218+ source : {
1219+ type : "bytes" ,
1220+ bytes : fileContent ,
1221+ } ,
1222+ } ;
1223+ await sendFileMessage ( ) ;
1224+ resolve ( ) ;
1225+ } ;
1226+
1227+ reader . readAsDataURL ( file ) ;
1228+ } ) ;
1229+ }
1230+ } else if ( rtvi_file . source . type === "bytes" ) {
1231+ const estimatedSize = rtvi_file . source . bytes . length + 1000 ;
1232+ if ( estimatedSize > this . _transport . maxMessageSize ) {
1233+ // Convert bytes to File and upload
1234+ const byteString = atob (
1235+ rtvi_file . source . bytes . split ( "," ) [ 1 ] || rtvi_file . source . bytes
1236+ ) ;
1237+ const ab = new ArrayBuffer ( byteString . length ) ;
1238+ const ia = new Uint8Array ( ab ) ;
1239+ for ( let i = 0 ; i < byteString . length ; i ++ ) {
1240+ ia [ i ] = byteString . charCodeAt ( i ) ;
1241+ }
1242+ const blob = new Blob ( [ ab ] , { type : mimeType } ) ;
1243+ uploadFile = new File ( [ blob ] , rtvi_file . name || "uploaded_file" , {
1244+ type : mimeType ,
1245+ } ) ;
1246+ }
1247+ }
1248+
1249+ if ( uploadFile ) {
1250+ // File is too large for transport, upload it first
1251+ rtvi_file = await this . uploadFile ( uploadFile ) ;
1252+ }
1253+
1254+ await sendFileMessage ( ) ;
1255+ }
1256+
1257+ /**
1258+ * Upload a file to a specified endpoint or the default files endpoint.
1259+ * @param file - The File to upload
1260+ * @param uploadFileParams - Optional APIRequest. If not provided, constructs
1261+ * endpoint from startBotParams.endpoint by replacing the path with /files
1262+ * @returns Promise resolving to RTVIFile with name, format, and FileUrl source
1263+ */
1264+ public async uploadFile (
1265+ file : File ,
1266+ uploadFileParams ?: APIRequest
1267+ ) : Promise < RTVIFile > {
1268+ let uploadUrl : string ;
1269+ let headers : Headers | undefined ;
1270+ let timeout : number | undefined ;
1271+
1272+ if ( uploadFileParams ) {
1273+ const { endpoint } = uploadFileParams ;
1274+ headers = uploadFileParams . headers ;
1275+ timeout = uploadFileParams . timeout ;
1276+
1277+ if ( endpoint instanceof URL ) {
1278+ uploadUrl = endpoint . toString ( ) ;
1279+ } else if ( typeof endpoint === "string" ) {
1280+ uploadUrl = endpoint ;
1281+ } else if (
1282+ typeof Request !== "undefined" &&
1283+ endpoint instanceof Request
1284+ ) {
1285+ uploadUrl = endpoint . url ;
1286+ } else {
1287+ throw new RTVIErrors . RTVIError (
1288+ "Unable to determine URL from uploadFileParams.endpoint"
1289+ ) ;
1290+ }
1291+ } else {
1292+ // Construct from startBotParams
1293+ const startBotParams = this . _transport . startBotParams ;
1294+ if ( ! startBotParams ?. endpoint ) {
1295+ throw new RTVIErrors . RTVIError (
1296+ "No uploadFileParams provided and no startBotParams.endpoint available"
1297+ ) ;
1298+ }
1299+
1300+ timeout = startBotParams . timeout ;
1301+
1302+ let baseUrl : URL ;
1303+ if ( startBotParams . endpoint instanceof URL ) {
1304+ baseUrl = startBotParams . endpoint ;
1305+ headers = startBotParams . headers ;
1306+ } else if ( typeof startBotParams . endpoint === "string" ) {
1307+ baseUrl = new URL ( startBotParams . endpoint ) ;
1308+ headers = startBotParams . headers ;
1309+ } else if (
1310+ typeof Request !== "undefined" &&
1311+ startBotParams . endpoint instanceof Request
1312+ ) {
1313+ baseUrl = new URL ( startBotParams . endpoint . url ) ;
1314+ headers = new Headers ( startBotParams . endpoint . headers ) ;
1315+ } else {
1316+ throw new RTVIErrors . RTVIError (
1317+ "Unable to determine base URL from startBotParams.endpoint"
1318+ ) ;
1319+ }
1320+
1321+ // Change the path to /files
1322+ uploadUrl = `${ baseUrl . origin } /files` ;
1323+ }
1324+
1325+ // Create FormData with the file
1326+ const formData = new FormData ( ) ;
1327+ formData . append ( "file" , file ) ;
1328+
1329+ // Create the Request object
1330+ // Note: Don't set Content-Type header - browser sets it automatically with boundary
1331+ const request = new Request ( uploadUrl , {
1332+ method : "POST" ,
1333+ mode : "cors" ,
1334+ body : formData ,
1335+ headers : headers ? Object . fromEntries ( headers . entries ( ) ) : undefined ,
1336+ } ) ;
1337+
1338+ const response = await makeRequest (
1339+ { endpoint : request , timeout } ,
1340+ this . _abortController
1341+ ) ;
1342+ return response as RTVIFile ;
1343+ }
1344+
11581345 /**
11591346 * Disconnects the bot, but keeps the session alive
11601347 */
@@ -1174,7 +1361,7 @@ export class PipecatClient extends RTVIEventEmitter {
11741361 : [ 0 , 0 , 0 ] ;
11751362 this . _botVersion = botVersion ;
11761363 logger . debug ( `[Pipecat Client] Bot is ready. Version: ${ data . version } ` ) ;
1177- if ( botVersion [ 0 ] < 2 ) {
1364+ if ( this . _botVersion [ 0 ] < 2 ) {
11781365 logger . warn (
11791366 `[Pipecat Client] Bot protocol version ${ data . version } is older than this client (${ RTVI_PROTOCOL_VERSION } ). Compatibility issues may occur.`
11801367 ) ;
0 commit comments