Repository: firebase/flutterfire
Pull request #17150: feat(vertexai): Unhandled ContentModality fix with more multimodal examples for vertexai testapp
Author: login: MichaelVerdon
Created: 2025-02-28T12:46:55Z | Merged: 2025-03-10T14:05:01Z

## Description

Fix 'Unhandle ContentModality' issue for audio.
closes https://github.com/firebase/flutterfire/issues/17133
Add an audio and text, video and text and document and text prompt examples for vertexai testapp to streamline processes of diagnosing audio related issues.

## Related Issues

*Replace this paragraph with a list of issues related to this PR from the [issue database](https://github.com/flutter/flutter/issues). Indicate, which of these issues are resolved or fixed by this PR. Note that you'll have to prefix the issue numbers with flutter/flutter#.*

## Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (`[x]`).
This will ensure a smooth and quick review process. Updating the `pubspec.yaml` and changelogs is not required.

- [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
- [ ] My PR includes unit or integration tests for *all* changed/updated/fixed behaviors (See [Contributor Guide]).
- [ ] All existing and new tests are passing.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] The analyzer (`melos run analyze`) does not report any problems on my PR.
- [ ] I read and followed the [Flutter Style Guide].
- [ ] I signed the [CLA].
- [ ] I am willing to follow-up on review comments in a timely manner.

## Breaking Change

Does your PR require plugin users to manually update their apps to accommodate your change?

- [ ] Yes, this is a breaking change.
- [ ] No, this is *not* a breaking change.

<!-- Links -->
[issue database]: https://github.com/flutter/flutter/issues
[Contributor Guide]: https://github.com/firebase/flutterfire/blob/main/CONTRIBUTING.md
[Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[pub versioning philosophy]: https://dart.dev/tools/pub/versioning
[CLA]: https://cla.developers.google.com/



Diff:
diff --git a/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/main/AndroidManifest.xml b/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/main/AndroidManifest.xml
index aff7dec7b850..8b7413a6a397 100644
--- a/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/main/AndroidManifest.xml
+++ b/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/main/AndroidManifest.xml
@@ -41,4 +41,7 @@
             <data android:mimeType="text/plain"/>
         </intent>
     </queries>
+    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
 </manifest>
diff --git a/packages/firebase_vertexai/firebase_vertexai/example/assets/documents/gemini_summary.pdf b/packages/firebase_vertexai/firebase_vertexai/example/assets/documents/gemini_summary.pdf
new file mode 100644
index 000000000000..08881c7839ec
Binary files /dev/null and b/packages/firebase_vertexai/firebase_vertexai/example/assets/documents/gemini_summary.pdf differ
diff --git a/packages/firebase_vertexai/firebase_vertexai/example/assets/videos/landscape.mp4 b/packages/firebase_vertexai/firebase_vertexai/example/assets/videos/landscape.mp4
new file mode 100644
index 000000000000..a7f4298dd7e9
Binary files /dev/null and b/packages/firebase_vertexai/firebase_vertexai/example/assets/videos/landscape.mp4 differ
diff --git a/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/project.pbxproj
index df60502d00f0..98cd0c1ded4a 100644
--- a/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/project.pbxproj
@@ -12,6 +12,7 @@
 		3414F5B6C6F086F6373F1948 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5F1FA05866A2D0FCA3287B20 /* GoogleService-Info.plist */; };
 		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
 		74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+		78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
 		901FEC83A38129064032C578 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CE5BFCDF90764354BB6740 /* Pods_Runner.framework */; };
 		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
 		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
@@ -82,6 +83,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
 				901FEC83A38129064032C578 /* Pods_Runner.framework in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -200,13 +202,16 @@
 				97C146EC1CF9000F007C117D /* Resources */,
 				9705A1C41CF9048500538489 /* Embed Frameworks */,
 				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
-				123ADD1BD119276C98000FAF /* [CP] Embed Pods Frameworks */,
+				34F21DFC67109DEAFD936E80 /* [CP] Embed Pods Frameworks */,
 			);
 			buildRules = (
 			);
 			dependencies = (
 			);
 			name = Runner;
+			packageProductDependencies = (
+				78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
+			);
 			productName = Runner;
 			productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
 			productType = "com.apple.product-type.application";
@@ -240,6 +245,9 @@
 				Base,
 			);
 			mainGroup = 97C146E51CF9000F007C117D;
+			packageReferences = (
+				781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
+			);
 			productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
 			projectDirPath = "";
 			projectRoot = "";
@@ -273,7 +281,7 @@
 /* End PBXResourcesBuildPhase section */
 
 /* Begin PBXShellScriptBuildPhase section */
-		123ADD1BD119276C98000FAF /* [CP] Embed Pods Frameworks */ = {
+		34F21DFC67109DEAFD936E80 /* [CP] Embed Pods Frameworks */ = {
 			isa = PBXShellScriptBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
@@ -729,6 +737,20 @@
 			defaultConfigurationName = Release;
 		};
 /* End XCConfigurationList section */
+
+/* Begin XCLocalSwiftPackageReference section */
+		781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
+			isa = XCLocalSwiftPackageReference;
+			relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
+		};
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+		78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
+			isa = XCSwiftPackageProductDependency;
+			productName = FlutterGeneratedPluginSwiftPackage;
+		};
+/* End XCSwiftPackageProductDependency section */
 	};
 	rootObject = 97C146E61CF9000F007C117D /* Project object */;
 }
diff --git a/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index d5bcd6878b4e..8178cd1c619c 100644
--- a/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -5,6 +5,24 @@
    <BuildAction
       parallelizeBuildables = "YES"
       buildImplicitDependencies = "YES">
+      <PreActions>
+         <ExecutionAction
+            ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
+            <ActionContent
+               title = "Run Prepare Flutter Framework Script"
+               scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
+               <EnvironmentBuildable>
+                  <BuildableReference
+                     BuildableIdentifier = "primary"
+                     BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+                     BuildableName = "Runner.app"
+                     BlueprintName = "Runner"
+                     ReferencedContainer = "container:Runner.xcodeproj">
+                  </BuildableReference>
+               </EnvironmentBuildable>
+            </ActionContent>
+         </ExecutionAction>
+      </PreActions>
       <BuildActionEntries>
          <BuildActionEntry
             buildForTesting = "YES"
@@ -59,6 +77,7 @@
       ignoresPersistentStateOnLaunch = "NO"
       debugDocumentVersioning = "YES"
       debugServiceExtension = "internal"
+      enableGPUValidationMode = "1"
       allowLocationSimulation = "YES">
       <BuildableProductRunnable
          runnableDebuggingMode = "0">
diff --git a/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner/Info.plist b/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner/Info.plist
index 5458fc4188bf..a80f00ea0116 100644
--- a/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner/Info.plist
+++ b/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner/Info.plist
@@ -45,5 +45,7 @@
 	<true/>
 	<key>UIApplicationSupportsIndirectInputEvents</key>
 	<true/>
+	<key>NSMicrophoneUsageDescription</key>
+	<string>We need access to the microphone to record audio.</string>
 </dict>
 </plist>
diff --git a/packages/firebase_vertexai/firebase_vertexai/example/lib/main.dart b/packages/firebase_vertexai/firebase_vertexai/example/lib/main.dart
index 0e3855eee06a..9f4e9fd4270e 100644
--- a/packages/firebase_vertexai/firebase_vertexai/example/lib/main.dart
+++ b/packages/firebase_vertexai/firebase_vertexai/example/lib/main.dart
@@ -18,11 +18,14 @@ import 'package:firebase_vertexai/firebase_vertexai.dart';
 import 'package:flutter/material.dart';
 
 import 'pages/chat_page.dart';
+import 'pages/audio_page.dart';
 import 'pages/function_calling_page.dart';
 import 'pages/image_prompt_page.dart';
 import 'pages/token_count_page.dart';
 import 'pages/schema_page.dart';
 import 'pages/imagen_page.dart';
+import 'pages/document.dart';
+import 'pages/video_page.dart';
 
 // REQUIRED if you want to run on Web
 const FirebaseOptions? options = null;
@@ -74,6 +77,7 @@ class _HomeScreenState extends State<HomeScreen> {
   List<Widget> get _pages => <Widget>[
         // Build _pages dynamically
         ChatPage(title: 'Chat', model: widget.model),
+        AudioPage(title: 'Audio', model: widget.model),
         TokenCountPage(title: 'Token Count', model: widget.model),
         const FunctionCallingPage(
           title: 'Function Calling',
@@ -81,6 +85,8 @@ class _HomeScreenState extends State<HomeScreen> {
         ImagePromptPage(title: 'Image Prompt', model: widget.model),
         ImagenPage(title: 'Imagen Model', model: widget.model),
         SchemaPromptPage(title: 'Schema Prompt', model: widget.model),
+        DocumentPage(title: 'Document Prompt', model: widget.model),
+        VideoPage(title: 'Video Prompt', model: widget.model),
       ];
 
   void _onItemTapped(int index) {
@@ -108,6 +114,14 @@ class _HomeScreenState extends State<HomeScreen> {
             label: 'Chat',
             tooltip: 'Chat',
           ),
+          BottomNavigationBarItem(
+            icon: Icon(
+              Icons.mic,
+              color: Theme.of(context).colorScheme.primary,
+            ),
+            label: 'Audio Prompt',
+            tooltip: 'Audio Prompt',
+          ),
           BottomNavigationBarItem(
             icon: Icon(
               Icons.numbers,
@@ -148,6 +162,22 @@ class _HomeScreenState extends State<HomeScreen> {
             label: 'Schema Prompt',
             tooltip: 'Schema Prompt',
           ),
+          BottomNavigationBarItem(
+            icon: Icon(
+              Icons.edit_document,
+              color: Theme.of(context).colorScheme.primary,
+            ),
+            label: 'Document Prompt',
+            tooltip: 'Document Prompt',
+          ),
+          BottomNavigationBarItem(
+            icon: Icon(
+              Icons.video_collection,
+              color: Theme.of(context).colorScheme.primary,
+            ),
+            label: 'Video Prompt',
+            tooltip: 'Video Prompt',
+          ),
         ],
         currentIndex: _selectedIndex,
         onTap: _onItemTapped,
diff --git a/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart
new file mode 100644
index 000000000000..63862e18f0e3
--- /dev/null
+++ b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart
@@ -0,0 +1,186 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:firebase_vertexai/firebase_vertexai.dart';
+import '../widgets/message_widget.dart';
+import 'package:record/record.dart';
+import 'package:path_provider/path_provider.dart';
+
+final record = AudioRecorder();
+
+class AudioPage extends StatefulWidget {
+  const AudioPage({super.key, required this.title, required this.model});
+
+  final String title;
+  final GenerativeModel model;
+
+  @override
+  State<AudioPage> createState() => _AudioPageState();
+}
+
+class _AudioPageState extends State<AudioPage> {
+  ChatSession? chat;
+  final ScrollController _scrollController = ScrollController();
+  final List<MessageData> _messages = <MessageData>[];
+  bool _recording = false;
+
+  @override
+  void initState() {
+    super.initState();
+    chat = widget.model.startChat();
+  }
+
+  Future<void> recordAudio() async {
+    if (!await record.hasPermission()) {
+      print('Audio recording permission denied');
+      return;
+    }
+
+    final dir = Directory(
+      '${(await getApplicationDocumentsDirectory()).path}/libs/recordings',
+    );
+
+    // ignore: avoid_slow_async_io
+    if (!await dir.exists()) {
+      await dir.create(recursive: true);
+    }
+
+    String filePath =
+        '${dir.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav';
+
+    await record.start(
+      const RecordConfig(
+        encoder: AudioEncoder.wav,
+      ),
+      path: filePath,
+    );
+  }
+
+  Future<void> stopRecord() async {
+    var path = await record.stop();
+
+    if (path == null) {
+      print('Failed to stop recording');
+      return;
+    }
+
+    debugPrint('Recording saved to: $path');
+
+    try {
+      File file = File(path);
+      final audio = await file.readAsBytes();
+      debugPrint('Audio file size: ${audio.length} bytes');
+
+      final audioPart = InlineDataPart('audio/wav', audio);
+
+      await _submitAudioToModel(audioPart);
+
+      await file.delete();
+      debugPrint('Recording deleted successfully.');
+    } catch (e) {
+      debugPrint('Error processing recording: $e');
+    }
+  }
+
+  Future<void> _submitAudioToModel(audioPart) async {
+    try {
+      String textPrompt = 'What is in the audio recording?';
+      final prompt = TextPart('What is in the audio recording?');
+
+      setState(() {
+        _messages.add(MessageData(text: textPrompt, fromUser: true));
+      });
+
+      final response = await widget.model.generateContent([
+        Content.multi([prompt, audioPart]),
+      ]);
+
+      setState(() {
+        _messages.add(MessageData(text: response.text, fromUser: false));
+      });
+
+      debugPrint(response.text);
+    } catch (e) {
+      debugPrint('Error sending audio to model: $e');
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(widget.title),
+      ),
+      body: Padding(
+        padding: const EdgeInsets.all(8),
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Expanded(
+              child: ListView.builder(
+                controller: _scrollController,
+                itemBuilder: (context, idx) {
+                  return MessageWidget(
+                    text: _messages[idx].text,
+                    image: _messages[idx].image,
+                    isFromUser: _messages[idx].fromUser ?? false,
+                  );
+                },
+                itemCount: _messages.length,
+              ),
+            ),
+            Padding(
+              padding: const EdgeInsets.symmetric(
+                vertical: 25,
+                horizontal: 15,
+              ),
+              child: Row(
+                children: [
+                  IconButton(
+                    onPressed: () async {
+                      setState(() {
+                        _recording = !_recording;
+                      });
+                      if (_recording) {
+                        await recordAudio();
+                      } else {
+                        await stopRecord();
+                      }
+                    },
+                    icon: Icon(
+                      Icons.mic,
+                      color: _recording
+                          ? Colors.blueGrey
+                          : Theme.of(context).colorScheme.primary,
+                    ),
+                  ),
+                  const SizedBox.square(
+                    dimension: 15,
+                  ),
+                  const Text(
+                    'Tap the mic to record, tap again to submit',
+                  ),
+                ],
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/document.dart b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/document.dart
new file mode 100644
index 000000000000..ff98680f429d
--- /dev/null
+++ b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/document.dart
@@ -0,0 +1,117 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:flutter/material.dart';
+import 'package:firebase_vertexai/firebase_vertexai.dart';
+import 'package:flutter/services.dart';
+import '../widgets/message_widget.dart';
+
+class DocumentPage extends StatefulWidget {
+  const DocumentPage({super.key, required this.title, required this.model});
+
+  final String title;
+  final GenerativeModel model;
+
+  @override
+  State<DocumentPage> createState() => _DocumentPageState();
+}
+
+class _DocumentPageState extends State<DocumentPage> {
+  ChatSession? chat;
+  late final GenerativeModel model;
+  final List<MessageData> _messages = <MessageData>[];
+  bool _loading = false;
+
+  @override
+  void initState() {
+    super.initState();
+    chat = widget.model.startChat();
+  }
+
+  Future<void> _testDocumentReading(model) async {
+    try {
+      ByteData docBytes =
+          await rootBundle.load('assets/documents/gemini_summary.pdf');
+
+      const _prompt =
+          'Write me a summary in one sentence what this document is about.';
+
+      final prompt = TextPart(_prompt);
+
+      setState(() {
+        _messages.add(MessageData(text: _prompt, fromUser: true));
+      });
+
+      final pdfPart =
+          InlineDataPart('application/pdf', docBytes.buffer.asUint8List());
+
+      final response = await widget.model.generateContent([
+        Content.multi([prompt, pdfPart]),
+      ]);
+
+      setState(() {
+        _messages.add(MessageData(text: response.text, fromUser: false));
+      });
+    } catch (e) {
+      print('Error sending document to model: $e');
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(widget.title),
+      ),
+      body: Padding(
+        padding: const EdgeInsets.all(8),
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Expanded(
+              child: ListView.builder(
+                itemBuilder: (context, idx) {
+                  return MessageWidget(
+                    text: _messages[idx].text,
+                    isFromUser: _messages[idx].fromUser ?? false,
+                  );
+                },
+                itemCount: _messages.length,
+              ),
+            ),
+            Padding(
+              padding: const EdgeInsets.symmetric(
+                vertical: 25,
+                horizontal: 15,
+              ),
+              child: Center(
+                child: SizedBox(
+                  child: ElevatedButton(
+                    onPressed: !_loading
+                        ? () async {
+                            await _testDocumentReading(widget.model);
+                          }
+                        : null,
+                    child: const Text('Test Document Reading'),
+                  ),
+                ),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/video_page.dart b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/video_page.dart
new file mode 100644
index 000000000000..532b981385f7
--- /dev/null
+++ b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/video_page.dart
@@ -0,0 +1,116 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:flutter/material.dart';
+import 'package:firebase_vertexai/firebase_vertexai.dart';
+import 'package:flutter/services.dart';
+import '../widgets/message_widget.dart';
+
+class VideoPage extends StatefulWidget {
+  const VideoPage({super.key, required this.title, required this.model});
+
+  final String title;
+  final GenerativeModel model;
+
+  @override
+  State<VideoPage> createState() => _VideoPageState();
+}
+
+class _VideoPageState extends State<VideoPage> {
+  ChatSession? chat;
+  late final GenerativeModel model;
+  final List<MessageData> _messages = <MessageData>[];
+  bool _loading = false;
+
+  @override
+  void initState() {
+    super.initState();
+    chat = widget.model.startChat();
+  }
+
+  Future<void> _testVideo(model) async {
+    try {
+      ByteData videoBytes =
+          await rootBundle.load('assets/videos/landscape.mp4');
+
+      const _prompt = 'Can you tell me what is in the video?';
+
+      final prompt = TextPart(_prompt);
+
+      setState(() {
+        _messages.add(MessageData(text: _prompt, fromUser: true));
+      });
+
+      final videoPart =
+          InlineDataPart('video/mp4', videoBytes.buffer.asUint8List());
+
+      final response = await widget.model.generateContent([
+        Content.multi([prompt, videoPart]),
+      ]);
+
+      setState(() {
+        _messages.add(MessageData(text: response.text, fromUser: false));
+      });
+    } catch (e) {
+      print('Error sending video to model: $e');
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(widget.title),
+      ),
+      body: Padding(
+        padding: const EdgeInsets.all(8),
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Expanded(
+              child: ListView.builder(
+                itemBuilder: (context, idx) {
+                  return MessageWidget(
+                    text: _messages[idx].text,
+                    isFromUser: _messages[idx].fromUser ?? false,
+                  );
+                },
+                itemCount: _messages.length,
+              ),
+            ),
+            Padding(
+              padding: const EdgeInsets.symmetric(
+                vertical: 25,
+                horizontal: 15,
+              ),
+              child: Center(
+                child: SizedBox(
+                  child: ElevatedButton(
+                    onPressed: !_loading
+                        ? () async {
+                            await _testVideo(widget.model);
+                          }
+                        : null,
+                    child: const Text('Test Video Prompt'),
+                  ),
+                ),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/packages/firebase_vertexai/firebase_vertexai/example/pubspec.yaml b/packages/firebase_vertexai/firebase_vertexai/example/pubspec.yaml
index 324c0c048e78..1e07df2e3f5b 100644
--- a/packages/firebase_vertexai/firebase_vertexai/example/pubspec.yaml
+++ b/packages/firebase_vertexai/firebase_vertexai/example/pubspec.yaml
@@ -25,6 +25,8 @@ dependencies:
   flutter:
     sdk: flutter
   flutter_markdown: ^0.6.20
+  path_provider: ^2.1.5
+  record: ^5.2.1
 
 dev_dependencies:
   flutter_lints: ^4.0.0
@@ -39,3 +41,5 @@ flutter:
   uses-material-design: true
   assets:
     - assets/images/
+    - assets/documents/
+    - assets/videos/
diff --git a/packages/firebase_vertexai/firebase_vertexai/lib/src/api.dart b/packages/firebase_vertexai/firebase_vertexai/lib/src/api.dart
index 3b3143fecfac..93ebc1480b49 100644
--- a/packages/firebase_vertexai/firebase_vertexai/lib/src/api.dart
+++ b/packages/firebase_vertexai/firebase_vertexai/lib/src/api.dart
@@ -531,9 +531,9 @@ enum ContentModality {
       'MODALITY_UNSPECIFIED' => ContentModality.unspecified,
       'TEXT' => ContentModality.text,
       'IMAGE' => ContentModality.image,
-      'video' => ContentModality.video,
-      'audio' => ContentModality.audio,
-      'document' => ContentModality.document,
+      'VIDEO' => ContentModality.video,
+      'AUDIO' => ContentModality.audio,
+      'DOCUMENT' => ContentModality.document,
       _ =>
         throw FormatException('Unhandled ContentModality format', jsonObject),
     };


Reviews:
user: login: russellwheatley
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-02-28T14:57:50Z
user: login: MichaelVerdon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-28T15:56:34Z
user: login: Lyokone
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T14:13:49Z
user: login: Lyokone
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T14:32:58Z
user: login: Lyokone
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T14:33:06Z
user: login: Lyokone
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T14:33:37Z
user: login: Lyokone
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T16:18:38Z
user: login: SelaseKay
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T16:20:40Z
user: login: cynthiajoan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T23:18:58Z
user: login: cynthiajoan
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-03T23:21:41Z
user: login: russellwheatley
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-05T16:38:51Z

Review comments:
diff_hunk: @@ -531,8 +531,8 @@ enum ContentModality {
       'MODALITY_UNSPECIFIED' => ContentModality.unspecified,
       'TEXT' => ContentModality.text,
       'IMAGE' => ContentModality.image,
path: packages/firebase_vertexai/firebase_vertexai/lib/src/api.dart
user: login: russellwheatley
body: worth finding out if it is a problem for the other modalities as well 😄 
created_at: 2025-02-28T14:57:47Z
author_association: MEMBER
diff_hunk: @@ -531,8 +531,8 @@ enum ContentModality {
       'MODALITY_UNSPECIFIED' => ContentModality.unspecified,
       'TEXT' => ContentModality.text,
       'IMAGE' => ContentModality.image,
path: packages/firebase_vertexai/firebase_vertexai/lib/src/api.dart
user: login: MichaelVerdon
body: According to the ticket. there were issues with Audio, Video and PDF, and I think this is all because of the strings there needing to match here: https://github.com/firebase/flutterfire/blob/a2246cd0ae8a7a53abc2537d7cd66ee079d3b096/packages/firebase_vertexai/firebase_vertexai/lib/src/api.dart#L508
created_at: 2025-02-28T15:56:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,184 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:firebase_vertexai/firebase_vertexai.dart';
+import '../widgets/message_widget.dart';
+import 'package:record/record.dart';
+import 'package:path_provider/path_provider.dart';
+
+final record = AudioRecorder();
+
+class AudioPage extends StatefulWidget {
+  const AudioPage({super.key, required this.title, required this.model});
+
+  final String title;
+  final GenerativeModel model;
+
+  @override
+  State<AudioPage> createState() => _AudioPageState();
+}
+
+
+
+class _AudioPageState extends State<AudioPage> {
+  ChatSession? chat;
+  final ScrollController _scrollController = ScrollController();
+  final List<MessageData> _messages = <MessageData>[];
+  bool _recording = false;
+
+    @override
+  void initState() {
+    super.initState();
+    chat = widget.model.startChat();
+  }
+
+  Future<void> recordAudio() async {
+    if (await record.hasPermission()) {
+      final dir = Directory(
+          '${(await getApplicationDocumentsDirectory()).path}/libs/recordings');
+
+      // ignore: avoid_slow_async_io
+      if (!await dir.exists()) {
+        await dir.create(recursive: true);
+      }
+
+      String filePath =
+          '${dir.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav';
+
+      await record.start(
+        const RecordConfig(
+          encoder: AudioEncoder.wav,
+        ),
+        path: filePath,
+      );
+    }
path: packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart
user: login: Lyokone
body: I think the code would be easier to read if you use early returns, something like:
```
if (!await record.hasPermission()) {
  print("Permission denied")
  return;
}

final dir = Directory(
    '${(await getApplicationDocumentsDirectory()).path}/libs/recordings');
...
```

It removes one level of indentation and makes it easier to debug
created_at: 2025-03-03T14:10:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,184 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:firebase_vertexai/firebase_vertexai.dart';
+import '../widgets/message_widget.dart';
+import 'package:record/record.dart';
+import 'package:path_provider/path_provider.dart';
+
+final record = AudioRecorder();
+
+class AudioPage extends StatefulWidget {
+  const AudioPage({super.key, required this.title, required this.model});
+
+  final String title;
+  final GenerativeModel model;
+
+  @override
+  State<AudioPage> createState() => _AudioPageState();
+}
+
+
+
+class _AudioPageState extends State<AudioPage> {
+  ChatSession? chat;
+  final ScrollController _scrollController = ScrollController();
+  final List<MessageData> _messages = <MessageData>[];
+  bool _recording = false;
+
+    @override
+  void initState() {
+    super.initState();
+    chat = widget.model.startChat();
+  }
+
+  Future<void> recordAudio() async {
+    if (await record.hasPermission()) {
+      final dir = Directory(
+          '${(await getApplicationDocumentsDirectory()).path}/libs/recordings');
+
+      // ignore: avoid_slow_async_io
+      if (!await dir.exists()) {
+        await dir.create(recursive: true);
+      }
+
+      String filePath =
+          '${dir.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav';
+
+      await record.start(
+        const RecordConfig(
+          encoder: AudioEncoder.wav,
+        ),
+        path: filePath,
+      );
+    }
+  }
+
+  Future<void> stopRecord() async {
+    final path = await record.stop();
+
+    if (path != null) {
+      debugPrint('Recording saved to: $path');
+
+      try {
+        File file = File(path);
+        final audio = await file.readAsBytes();
+        debugPrint('Audio file size: ${audio.length} bytes');
+
+        final audioPart = InlineDataPart('audio/wav', audio);
path: packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart
user: login: Lyokone
body: Same here
created_at: 2025-03-03T14:10:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,184 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:firebase_vertexai/firebase_vertexai.dart';
+import '../widgets/message_widget.dart';
+import 'package:record/record.dart';
+import 'package:path_provider/path_provider.dart';
+
+final record = AudioRecorder();
+
+class AudioPage extends StatefulWidget {
+  const AudioPage({super.key, required this.title, required this.model});
+
+  final String title;
+  final GenerativeModel model;
+
+  @override
+  State<AudioPage> createState() => _AudioPageState();
+}
+
+
+
+class _AudioPageState extends State<AudioPage> {
+  ChatSession? chat;
+  final ScrollController _scrollController = ScrollController();
+  final List<MessageData> _messages = <MessageData>[];
+  bool _recording = false;
+
+    @override
+  void initState() {
+    super.initState();
+    chat = widget.model.startChat();
+  }
+
+  Future<void> recordAudio() async {
+    if (await record.hasPermission()) {
+      final dir = Directory(
+          '${(await getApplicationDocumentsDirectory()).path}/libs/recordings');
+
+      // ignore: avoid_slow_async_io
+      if (!await dir.exists()) {
+        await dir.create(recursive: true);
+      }
+
+      String filePath =
+          '${dir.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav';
+
+      await record.start(
+        const RecordConfig(
+          encoder: AudioEncoder.wav,
+        ),
+        path: filePath,
+      );
+    }
+  }
+
+  Future<void> stopRecord() async {
+    final path = await record.stop();
+
+    if (path != null) {
+      debugPrint('Recording saved to: $path');
+
+      try {
+        File file = File(path);
+        final audio = await file.readAsBytes();
+        debugPrint('Audio file size: ${audio.length} bytes');
+
+        final audioPart = InlineDataPart('audio/wav', audio);
+
+        await _submitAudioToModel(audioPart);
+
+        await file.delete();
+        debugPrint('Recording deleted successfully.');
+      } catch (e) {
+        debugPrint('Error processing recording: $e');
+      }
+    } else {
+      debugPrint('Failed to stop recording.');
+    }
+  }
+
+  Future<void> _submitAudioToModel(audioPart) async {
+    try {
+
path: packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart
user: login: Lyokone
body: You can remove the extra space
created_at: 2025-03-03T14:11:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,184 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:firebase_vertexai/firebase_vertexai.dart';
+import '../widgets/message_widget.dart';
+import 'package:record/record.dart';
+import 'package:path_provider/path_provider.dart';
+
+final record = AudioRecorder();
+
+class AudioPage extends StatefulWidget {
+  const AudioPage({super.key, required this.title, required this.model});
+
+  final String title;
+  final GenerativeModel model;
+
+  @override
+  State<AudioPage> createState() => _AudioPageState();
+}
+
+
+
+class _AudioPageState extends State<AudioPage> {
+  ChatSession? chat;
+  final ScrollController _scrollController = ScrollController();
+  final List<MessageData> _messages = <MessageData>[];
+  bool _recording = false;
+
+    @override
+  void initState() {
+    super.initState();
+    chat = widget.model.startChat();
+  }
+
+  Future<void> recordAudio() async {
+    if (await record.hasPermission()) {
+      final dir = Directory(
+          '${(await getApplicationDocumentsDirectory()).path}/libs/recordings');
+
+      // ignore: avoid_slow_async_io
+      if (!await dir.exists()) {
+        await dir.create(recursive: true);
+      }
+
+      String filePath =
+          '${dir.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav';
+
+      await record.start(
+        const RecordConfig(
+          encoder: AudioEncoder.wav,
+        ),
+        path: filePath,
+      );
+    }
+  }
+
+  Future<void> stopRecord() async {
+    final path = await record.stop();
+
+    if (path != null) {
+      debugPrint('Recording saved to: $path');
+
+      try {
+        File file = File(path);
+        final audio = await file.readAsBytes();
+        debugPrint('Audio file size: ${audio.length} bytes');
+
+        final audioPart = InlineDataPart('audio/wav', audio);
+
+        await _submitAudioToModel(audioPart);
+
+        await file.delete();
+        debugPrint('Recording deleted successfully.');
+      } catch (e) {
+        debugPrint('Error processing recording: $e');
+      }
+    } else {
+      debugPrint('Failed to stop recording.');
+    }
+  }
+
+  Future<void> _submitAudioToModel(audioPart) async {
+    try {
+
+      String textPrompt = 'What is in the audio recording?';
+      final prompt = TextPart('What is in the audio recording?');
+      setState(() {
+        _messages.add(MessageData(text: textPrompt, fromUser: true));
+      });
+      
+
path: packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart
user: login: Lyokone
body: Here as well
created_at: 2025-03-03T14:11:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,184 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:firebase_vertexai/firebase_vertexai.dart';
+import '../widgets/message_widget.dart';
+import 'package:record/record.dart';
+import 'package:path_provider/path_provider.dart';
+
+final record = AudioRecorder();
+
+class AudioPage extends StatefulWidget {
+  const AudioPage({super.key, required this.title, required this.model});
+
+  final String title;
+  final GenerativeModel model;
+
+  @override
+  State<AudioPage> createState() => _AudioPageState();
+}
+
+
+
+class _AudioPageState extends State<AudioPage> {
+  ChatSession? chat;
+  final ScrollController _scrollController = ScrollController();
+  final List<MessageData> _messages = <MessageData>[];
+  bool _recording = false;
+
+    @override
+  void initState() {
+    super.initState();
+    chat = widget.model.startChat();
+  }
+
+  Future<void> recordAudio() async {
+    if (await record.hasPermission()) {
+      final dir = Directory(
+          '${(await getApplicationDocumentsDirectory()).path}/libs/recordings');
+
+      // ignore: avoid_slow_async_io
+      if (!await dir.exists()) {
+        await dir.create(recursive: true);
+      }
+
+      String filePath =
+          '${dir.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav';
+
+      await record.start(
+        const RecordConfig(
+          encoder: AudioEncoder.wav,
+        ),
+        path: filePath,
+      );
+    }
+  }
+
+  Future<void> stopRecord() async {
+    final path = await record.stop();
+
+    if (path != null) {
+      debugPrint('Recording saved to: $path');
+
+      try {
+        File file = File(path);
+        final audio = await file.readAsBytes();
+        debugPrint('Audio file size: ${audio.length} bytes');
+
+        final audioPart = InlineDataPart('audio/wav', audio);
+
+        await _submitAudioToModel(audioPart);
+
+        await file.delete();
+        debugPrint('Recording deleted successfully.');
+      } catch (e) {
+        debugPrint('Error processing recording: $e');
+      }
+    } else {
+      debugPrint('Failed to stop recording.');
+    }
+  }
+
+  Future<void> _submitAudioToModel(audioPart) async {
+    try {
+
+      String textPrompt = 'What is in the audio recording?';
+      final prompt = TextPart('What is in the audio recording?');
+      setState(() {
+        _messages.add(MessageData(text: textPrompt, fromUser: true));
+      });
+      
+
+      final response = await widget.model.generateContent([
+        Content.multi([prompt, audioPart]),
+      ]);
+      setState(() {
+        _messages.add(MessageData(text: response.text, fromUser: false));
+      });
+
+
path: packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart
user: login: Lyokone
body: Same
created_at: 2025-03-03T14:11:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,121 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:typed_data';
+
+import 'package:flutter/material.dart';
+import 'package:firebase_vertexai/firebase_vertexai.dart';
+import 'package:flutter/services.dart';
+import '../widgets/message_widget.dart';
+
+class DocumentPage extends StatefulWidget {
+  const DocumentPage({super.key, required this.title, required this.model});
+
+  final String title;
+  final GenerativeModel model;
+
+  @override
+  State<DocumentPage> createState() => _DocumentPageState();
+}
+
+class _DocumentPageState extends State<DocumentPage> {
+  ChatSession? chat;
+  late final GenerativeModel model;
+  final List<MessageData> _messages = <MessageData>[];
+  bool _loading = false;
+
+  @override
+  void initState() {
+    super.initState();
+    chat = widget.model.startChat();
+  }
+
+  Future<void> _testDocumentReading(model) async {
+    try {
+      ByteData docBytes =
+          await rootBundle.load('assets/documents/gemini_summary.pdf');
+
+      const _prompt =
+          'Write me a summary in one sentence what this document is about.';
+
+      final prompt = TextPart(_prompt);
+
+      setState(() {
+        _messages.add(MessageData(text: _prompt, fromUser: true));
+      });
+
+      final pdfPart = InlineDataPart('application/pdf', docBytes.buffer.asUint8List());
+
+      final response = await widget.model.generateContent([
+        Content.multi([prompt, pdfPart]),
+      ]);
+
+      setState((){
+        _messages.add(MessageData(text: response.text, fromUser: false));
+      });
+      
+    } catch (e) {
+      print('Error sending document to model: $e');
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(widget.title),
+      ),
+      body: Padding(
+        padding: const EdgeInsets.all(8),
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Expanded(
+              child: ListView.builder(
+                itemBuilder: (context, idx) {
+                  return MessageWidget(
+                    text: _messages[idx].text,
+                    isFromUser: _messages[idx].fromUser ?? false,
+                  );
+                },
+                itemCount: _messages.length,
+              ),
+            ),
+            Padding(
+              padding: const EdgeInsets.symmetric(
+                vertical: 25,
+                horizontal: 15,
+              ),
+              child: Row(
path: packages/firebase_vertexai/firebase_vertexai/example/lib/pages/document.dart
user: login: Lyokone
body: It's a Row with only one child if I'm not mistaken, probably worth cleaning
created_at: 2025-03-03T14:12:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,121 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:typed_data';
+
+import 'package:flutter/material.dart';
+import 'package:firebase_vertexai/firebase_vertexai.dart';
+import 'package:flutter/services.dart';
+import '../widgets/message_widget.dart';
+
+class VideoPage extends StatefulWidget {
+  const VideoPage({super.key, required this.title, required this.model});
+
+  final String title;
+  final GenerativeModel model;
+
+  @override
+  State<VideoPage> createState() => _VideoPageState();
+}
+
+class _VideoPageState extends State<VideoPage> {
+  ChatSession? chat;
+  late final GenerativeModel model;
+  final List<MessageData> _messages = <MessageData>[];
+  bool _loading = false;
+
+  @override
+  void initState() {
+    super.initState();
+    chat = widget.model.startChat();
+  }
+
+  Future<void> _testVideo(model) async {
+    try {
+      ByteData videoBytes =
+          await rootBundle.load('assets/videos/landscape.mp4');
+
+      const _prompt =
+          'Can you tell me what is in the video?';
+
+      final prompt = TextPart(_prompt);
+
+      setState(() {
+        _messages.add(MessageData(text: _prompt, fromUser: true));
+      });
+
+      final videoPart = InlineDataPart('video/mp4', videoBytes.buffer.asUint8List());
+
+      final response = await widget.model.generateContent([
+        Content.multi([prompt, videoPart]),
+      ]);
+
+      setState((){
+        _messages.add(MessageData(text: response.text, fromUser: false));
+      });
+      
+    } catch (e) {
+      print('Error sending video to model: $e');
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(widget.title),
+      ),
+      body: Padding(
+        padding: const EdgeInsets.all(8),
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Expanded(
+              child: ListView.builder(
+                itemBuilder: (context, idx) {
+                  return MessageWidget(
+                    text: _messages[idx].text,
+                    isFromUser: _messages[idx].fromUser ?? false,
+                  );
+                },
+                itemCount: _messages.length,
+              ),
+            ),
+            Padding(
+              padding: const EdgeInsets.symmetric(
+                vertical: 25,
+                horizontal: 15,
+              ),
+              child: Row(
+                children: [
+                  Expanded(
+                    child: ElevatedButton(
+                      onPressed: !_loading
+                          ? () async {
path: packages/firebase_vertexai/firebase_vertexai/example/lib/pages/video_page.dart
user: login: Lyokone
body: Same
created_at: 2025-03-03T14:13:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,186 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:firebase_vertexai/firebase_vertexai.dart';
+import '../widgets/message_widget.dart';
+import 'package:record/record.dart';
+import 'package:path_provider/path_provider.dart';
+
+final record = AudioRecorder();
+
+class AudioPage extends StatefulWidget {
+  const AudioPage({super.key, required this.title, required this.model});
+
+  final String title;
+  final GenerativeModel model;
+
+  @override
+  State<AudioPage> createState() => _AudioPageState();
+}
+
+
+
+class _AudioPageState extends State<AudioPage> {
+  ChatSession? chat;
+  final ScrollController _scrollController = ScrollController();
+  final List<MessageData> _messages = <MessageData>[];
+  bool _recording = false;
+
+    @override
+  void initState() {
+    super.initState();
+    chat = widget.model.startChat();
+  }
+
+  Future<void> recordAudio() async {
+    if (!await record.hasPermission()) {
+      print('Audio recording permission denied');
+      return;
+    }
+
+      final dir = Directory(
+          '${(await getApplicationDocumentsDirectory()).path}/libs/recordings');
+
+      // ignore: avoid_slow_async_io
path: packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart
user: login: Lyokone
body: Indentation issue I believe
created_at: 2025-03-03T14:32:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,186 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:firebase_vertexai/firebase_vertexai.dart';
+import '../widgets/message_widget.dart';
+import 'package:record/record.dart';
+import 'package:path_provider/path_provider.dart';
+
+final record = AudioRecorder();
+
+class AudioPage extends StatefulWidget {
+  const AudioPage({super.key, required this.title, required this.model});
+
+  final String title;
+  final GenerativeModel model;
+
+  @override
+  State<AudioPage> createState() => _AudioPageState();
+}
+
+
+
+class _AudioPageState extends State<AudioPage> {
+  ChatSession? chat;
+  final ScrollController _scrollController = ScrollController();
+  final List<MessageData> _messages = <MessageData>[];
+  bool _recording = false;
+
+    @override
+  void initState() {
+    super.initState();
+    chat = widget.model.startChat();
+  }
+
+  Future<void> recordAudio() async {
+    if (!await record.hasPermission()) {
+      print('Audio recording permission denied');
+      return;
+    }
+
+      final dir = Directory(
+          '${(await getApplicationDocumentsDirectory()).path}/libs/recordings');
+
+      // ignore: avoid_slow_async_io
+      if (!await dir.exists()) {
+        await dir.create(recursive: true);
+      }
+
+      String filePath =
+          '${dir.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav';
+
+      await record.start(
+        const RecordConfig(
+          encoder: AudioEncoder.wav,
+        ),
+        path: filePath,
+      );
+  }
+
+  Future<void> stopRecord() async {
+    var path = await record.stop();
+
+    if (path == null) {
+      print('Failed to stop recording');
+      return;
+    }
+
+      debugPrint('Recording saved to: $path');
+
+      try {
path: packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart
user: login: Lyokone
body: Same here
created_at: 2025-03-03T14:33:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,121 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:typed_data';
+
+import 'package:flutter/material.dart';
+import 'package:firebase_vertexai/firebase_vertexai.dart';
+import 'package:flutter/services.dart';
+import '../widgets/message_widget.dart';
+
+class VideoPage extends StatefulWidget {
+  const VideoPage({super.key, required this.title, required this.model});
+
+  final String title;
+  final GenerativeModel model;
+
+  @override
+  State<VideoPage> createState() => _VideoPageState();
+}
+
+class _VideoPageState extends State<VideoPage> {
+  ChatSession? chat;
+  late final GenerativeModel model;
+  final List<MessageData> _messages = <MessageData>[];
+  bool _loading = false;
+
+  @override
+  void initState() {
+    super.initState();
+    chat = widget.model.startChat();
+  }
+
+  Future<void> _testVideo(model) async {
+    try {
+      ByteData videoBytes =
+          await rootBundle.load('assets/videos/landscape.mp4');
+
+      const _prompt =
+          'Can you tell me what is in the video?';
+
+      final prompt = TextPart(_prompt);
+
+      setState(() {
+        _messages.add(MessageData(text: _prompt, fromUser: true));
+      });
+
+      final videoPart = InlineDataPart('video/mp4', videoBytes.buffer.asUint8List());
+
+      final response = await widget.model.generateContent([
+        Content.multi([prompt, videoPart]),
+      ]);
+
+      setState((){
+        _messages.add(MessageData(text: response.text, fromUser: false));
+      });
+      
+    } catch (e) {
+      print('Error sending video to model: $e');
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(widget.title),
+      ),
+      body: Padding(
+        padding: const EdgeInsets.all(8),
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Expanded(
+              child: ListView.builder(
+                itemBuilder: (context, idx) {
+                  return MessageWidget(
+                    text: _messages[idx].text,
+                    isFromUser: _messages[idx].fromUser ?? false,
+                  );
+                },
+                itemCount: _messages.length,
+              ),
+            ),
+            Padding(
+              padding: const EdgeInsets.symmetric(
+                vertical: 25,
+                horizontal: 15,
+              ),
+              child: Row(
+                children: [
+                  Expanded(
+                    child: ElevatedButton(
+                      onPressed: !_loading
+                          ? () async {
path: packages/firebase_vertexai/firebase_vertexai/example/lib/pages/video_page.dart
user: login: Lyokone
body: I'm still seeing the Row with single children here
created_at: 2025-03-03T14:33:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,187 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:firebase_vertexai/firebase_vertexai.dart';
+import '../widgets/message_widget.dart';
+import 'package:record/record.dart';
+import 'package:path_provider/path_provider.dart';
+
+final record = AudioRecorder();
+
+class AudioPage extends StatefulWidget {
+  const AudioPage({super.key, required this.title, required this.model});
+
+  final String title;
+  final GenerativeModel model;
+
+  @override
+  State<AudioPage> createState() => _AudioPageState();
+}
+
+class _AudioPageState extends State<AudioPage> {
+  ChatSession? chat;
+  final ScrollController _scrollController = ScrollController();
+  final List<MessageData> _messages = <MessageData>[];
+  bool _recording = false;
+
+  @override
+  void initState() {
+    super.initState();
+    chat = widget.model.startChat();
+  }
+
+  Future<void> recordAudio() async {
+    if (!await record.hasPermission()) {
+      print('Audio recording permission denied');
+      return;
+    }
+
+    final dir = Directory(
+      '${(await getApplicationDocumentsDirectory()).path}/libs/recordings',
+    );
+
+    // ignore: avoid_slow_async_io
+    if (!await dir.exists()) {
+      await dir.create(recursive: true);
+    }
+
+    String filePath =
+        '${dir.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav';
+
+    await record.start(
+      const RecordConfig(
+        encoder: AudioEncoder.wav,
+      ),
+      path: filePath,
+    );
+  }
+
+  Future<void> stopRecord() async {
+    var path = await record.stop();
+
+    if (path == null) {
+      print('Failed to stop recording');
+      return;
+    }
+
+    debugPrint('Recording saved to: $path');
+
+    try {
+      File file = File(path);
+      final audio = await file.readAsBytes();
+      debugPrint('Audio file size: ${audio.length} bytes');
+
+      final audioPart = InlineDataPart('audio/wav', audio);
+
+      await _submitAudioToModel(audioPart);
+
+      await file.delete();
+      debugPrint('Recording deleted successfully.');
+    } catch (e) {
+      debugPrint('Error processing recording: $e');
+    }
+  }
+
+  Future<void> _submitAudioToModel(audioPart) async {
+    try {
+      String textPrompt = 'What is in the audio recording?';
+      final prompt = TextPart('What is in the audio recording?');
+
+      setState(() {
+        _messages.add(MessageData(text: textPrompt, fromUser: true));
+      });
+
+      final response = await widget.model.generateContent([
+        Content.multi([prompt, audioPart]),
+      ]);
+
+      setState(() {
+        _messages.add(MessageData(text: response.text, fromUser: false));
+      });
+
+      print(response.text);
+      debugPrint(response.text);
path: packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart
user: login: cynthiajoan
body: Do we need both?
created_at: 2025-03-03T23:18:57Z
author_association: COLLABORATOR

PR comments:
user: login: suesitran
created_at: 2025-03-02T23:28:00Z
body: I come here from this issue https://github.com/firebase/flutterfire/issues/17133

and this actually works for all types of files. When will it be released?
author_association: NONE
user: login: MichaelVerdon
created_at: 2025-03-03T14:09:24Z
body: > I come here from this issue #17133
> 
> and this actually works for all types of files. When will it be released?

Hi there, yes it works for all types of files. if the PR is Merged then the release can come as soon as tomorrow.
author_association: CONTRIBUTOR
user: login: mohsinhundekar
created_at: 2025-03-04T03:30:05Z
body: When this PR will be merged?
author_association: NONE
user: login: mohsinhundekar
created_at: 2025-03-05T17:17:10Z
body: I see changes were pushed and got approved as well, now when this open PR will be merged?


author_association: NONE
user: login: suesitran
created_at: 2025-03-06T16:14:39Z
body: i really need this PR to be merged because somehow Codemagic just decided to pick on this PR and stop building my app 😆 
https://discord.com/channels/1131597315707261018/1347124338561777674/1347124338561777674
author_association: NONE
user: login: mohsinhundekar
created_at: 2025-03-08T21:15:11Z
body: any update on this PR?
author_association: NONE
user: login: mohsinhundekar
created_at: 2025-03-16T10:52:30Z
body: I see that this PR has been merged into the main branch, but it’s still not available on pub.dev. I’m using the latest version (1.4.0) of this package, but the issue persists. Could someone provide an update on this? I appreciate all your hard work!
author_association: NONE
user: login: mohsinhundekar
created_at: 2025-03-16T10:53:29Z
body: @Lyokone I see that this PR has been merged into the main branch, but it’s still not available on pub.dev. I’m using the latest version (1.4.0) of this package, but the issue persists. Could someone provide an update on this? I appreciate all your hard work!
author_association: NONE