iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 30
0
自我挑戰組

從零開始的Flutter世界系列 第 30

Day30 Flutter Camera、播放影片

  • 分享至 

  • xImage
  •  

鐵人賽完賽了... 很抱歉沒有完成一開始訂的完成一個旅遊App 的目標,最終只能算是完成Flutter 的入門介紹,最後一天才決定要報名再加上中間遇到一些困難,導致有很多想要講的並沒有時間完成,之後我會在Medium新建一個系列來完成內容,也會對之前講的一些不足的地方做補充,有興趣的朋友歡迎再去看看,感恩各位
最近應該會回去將前面覺得不足的文章做補充,之後也會把範例完成,歡迎各位再賞臉去看看

今天最後介紹一些小工具

Camera

很多App都會有功能要使用到手機等設備的相機功能拍攝圖片和影片,因此,Flutter提供了camera插件,camera插件提供了一系列可用的相機功能,可以使用相機預覽、拍照、錄影片

添加三個依賴:

  1. camera
    提供使用設備相機模組的工具
  2. path_provider
    尋找儲存圖片的正確路徑
  3. path
    創建適配任何平台的路徑
...
dependencies:
  flutter:
    sdk: flutter
  camera: ^0.5.8
  path_provider: ^1.6.21
  path: ^1.7.0
...  

在Android,您必須更新minSdkVersion到21(或更高)

在iOS上,在ios/Runner/Info.plist中添加下面幾行才能使用相機

<key>NSCameraUsageDescription</key>
<string>Explanation on why the camera access is needed.</string>

獲取可用相機列表

使用camera插件獲取可用相機列表

// Ensure that plugin services are initialized so that `availableCameras()`
// can be called before `runApp()`
WidgetsFlutterBinding.ensureInitialized();

// Obtain a list of the available cameras on the device.
final cameras = await availableCameras();

// Get a specific camera from the list of available cameras.
final firstCamera = cameras.first;

創建並初始化 CameraController

在選擇了一個相機後,你需要創建並初始化CameraController。在這個過程中,與設備相機建立了連接並允許你控制相機並展示相機的預覽

如果你沒有初始化CameraController,你就不能使用相機預覽和拍照

實現這個過程,請依照以下步驟:

  1. 創建一個帶有State類的StatefulWidget組件
  2. 添加一個變量到State類來存放CameraController
  3. 添加另外一個變量到State類中來存放 CameraController.initialize()返回的Future
  4. initState()方法中創建並初始化控制器
  5. dispose()方法中銷毀控制器
// A screen that takes in a list of cameras and the Directory to store images.
class TakePictureScreen extends StatefulWidget {
  final CameraDescription camera;

  const TakePictureScreen({
    Key key,
    @required this.camera,
  }) : super(key: key);

  @override
  TakePictureScreenState createState() => TakePictureScreenState();
}

class TakePictureScreenState extends State<TakePictureScreen> {
  // Add two variables to the state class to store the CameraController and
  // the Future.
  CameraController _controller;
  Future<void> _initializeControllerFuture;

  @override
  void initState() {
    super.initState();
    // To display the current output from the camera,
    // create a CameraController.
    _controller = CameraController(
      // Get a specific camera from the list of available cameras.
      widget.camera,
      // Define the resolution to use.
      ResolutionPreset.medium,
    );

    // Next, initialize the controller. This returns a Future.
    _initializeControllerFuture = _controller.initialize();
  }

  @override
  void dispose() {
    // Dispose of the controller when the widget is disposed.
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // Fill this out in the next steps.
  }
}

initState方法中創建並初始化控制器

使用camera中的CameraPreview組件來展示相機預覽,在使用相機前,請確保控制器已經完成初始化。因此,你一定要等待前一個步驟創建_initializeControllerFuture() 執行完畢才去展示CameraPreview

// You must wait until the controller is initialized before displaying the
// camera preview. Use a FutureBuilder to display a loading spinner until the
// controller has finished initializing.
FutureBuilder<void>(
  future: _initializeControllerFuture,
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.done) {
      // If the Future is complete, display the preview.
      return CameraPreview(_controller);
    } else {
      // Otherwise, display a loading indicator.
      return Center(child: CircularProgressIndicator());
    }
  },
)

使用CameraController拍照

可以使用CameraControllertakePicture()方法拍照。在這個示例中,創建了一個浮動按鈕FloatingActionButton,當使用者點擊這個按鈕,就能通過CameraController來拍攝圖片

保存一張圖片,需要經過一下三個步驟:

  1. 確保相機模塊已經被初始化完成
  2. 創建圖片需要被保存的路徑
  3. 使用控制器拍攝一張圖片並保存結果到上述路徑

建議把這些操作都放在try / catch方法區塊中來處理可能發生的異常

FloatingActionButton(
  child: Icon(Icons.camera_alt),
  // Provide an onPressed callback.
  onPressed: () async {
    // Take the Picture in a try / catch block. If anything goes wrong,
    // catch the error.
    try {
      // Ensure that the camera is initialized.
      await _initializeControllerFuture;

      // Construct the path where the image should be saved using the path
      // package.
      final path = join(
        // Store the picture in the temp directory.
        // Find the temp directory using the `path_provider` plugin.
        (await getTemporaryDirectory()).path,
        '${DateTime.now()}.png',
      );

      // Attempt to take a picture and log where it's been saved.
      await _controller.takePicture(path);
    } catch (e) {
      // If an error occurs, log the error to the console.
      print(e);
    }
  },
)

Imagewidget 顯示圖片

如果你能成功拍攝圖片,你就可以使用Image組件展示所保存的圖片。在這個示例中,這張圖片是以文件的形式儲存在設備中

因此,你需要提供一個FileImage.file建構函數。你能夠通過傳遞你在上一步中創建的路徑來創建一個File類的實例

Image.file(File('path/to/my/picture.png'))

完整程式碼範例:

import 'dart:async';
import 'dart:io';

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' show join;
import 'package:path_provider/path_provider.dart';

Future<void> main() async {
  // Ensure that plugin services are initialized so that `availableCameras()`
  // can be called before `runApp()`
  WidgetsFlutterBinding.ensureInitialized();

  // Obtain a list of the available cameras on the device.
  final cameras = await availableCameras();

  // Get a specific camera from the list of available cameras.
  final firstCamera = cameras.first;

  runApp(
    MaterialApp(
      theme: ThemeData.dark(),
      home: TakePictureScreen(
        // Pass the appropriate camera to the TakePictureScreen widget.
        camera: firstCamera,
      ),
    ),
  );
}

// A screen that allows users to take a picture using a given camera.
class TakePictureScreen extends StatefulWidget {
  final CameraDescription camera;

  const TakePictureScreen({
    Key key,
    @required this.camera,
  }) : super(key: key);

  @override
  TakePictureScreenState createState() => TakePictureScreenState();
}

class TakePictureScreenState extends State<TakePictureScreen> {
  CameraController _controller;
  Future<void> _initializeControllerFuture;

  @override
  void initState() {
    super.initState();
    // To display the current output from the Camera,
    // create a CameraController.
    _controller = CameraController(
      // Get a specific camera from the list of available cameras.
      widget.camera,
      // Define the resolution to use.
      ResolutionPreset.medium,
    );

    // Next, initialize the controller. This returns a Future.
    _initializeControllerFuture = _controller.initialize();
  }

  @override
  void dispose() {
    // Dispose of the controller when the widget is disposed.
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Take a picture')),
      // Wait until the controller is initialized before displaying the
      // camera preview. Use a FutureBuilder to display a loading spinner
      // until the controller has finished initializing.
      body: FutureBuilder<void>(
        future: _initializeControllerFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            // If the Future is complete, display the preview.
            return CameraPreview(_controller);
          } else {
            // Otherwise, display a loading indicator.
            return Center(child: CircularProgressIndicator());
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.camera_alt),
        // Provide an onPressed callback.
        onPressed: () async {
          // Take the Picture in a try / catch block. If anything goes wrong,
          // catch the error.
          try {
            // Ensure that the camera is initialized.
            await _initializeControllerFuture;

            // Construct the path where the image should be saved using the
            // pattern package.
            final path = join(
              // Store the picture in the temp directory.
              // Find the temp directory using the `path_provider` plugin.
              (await getTemporaryDirectory()).path,
              '${DateTime.now()}.png',
            );

            // Attempt to take a picture and log where it's been saved.
            await _controller.takePicture(path);

            // If the picture was taken, display it on a new screen.
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => DisplayPictureScreen(imagePath: path),
              ),
            );
          } catch (e) {
            // If an error occurs, log the error to the console.
            print(e);
          }
        },
      ),
    );
  }
}

// A widget that displays the picture taken by the user.
class DisplayPictureScreen extends StatelessWidget {
  final String imagePath;

  const DisplayPictureScreen({Key key, this.imagePath}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Display the Picture')),
      // The image is stored as a file on the device. Use the `Image.file`
      // constructor with the given path to display the image.
      body: Image.file(File(imagePath)),
    );
  }
}

影片的widget

為了支持影片播放,Flutter團隊提供了video_player插件。你可以使用video_player插件播放存在本地文件系統中的影片或者網路影片,在iOS上,video_player使用AVPlayer進行播放控制。在Android上,使用的是ExoPlayer

接下來介紹我們是如何借助video_player包接收網路影片流,並加入基本的播放、暫停操作

添加video_player依賴

...
dependencies:
  flutter:
    sdk: flutter
  video_player: ^0.11.1
...  

添加權限

  • Android 配置:

    AndroidManifest.xml文件中的<application>配置項下加入如下權限。 AndroidManifest.xml文件的路徑是 <project root>/android/app/src/main/AndroidManifest.xml

    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
        <application ...>
    
        </application>
    
        <uses-permission android:name="android.permission.INTERNET"/>
    </manifest>
    
  • iOS 配置:

    針對iOS,你需要在<project root>/ios/Runner/Info.plist 路徑下的Info.plist文件中加入如下配置

    <key>NSAppTransportSecurity</key>
    <dict>
      <key>NSAllowsArbitraryLoads</key>
      <true/>
    </dict>
    

    video_player 插件在iOS 模擬器上不能使用,必須要在iOS 真機上進行測試

創建並初始化 VideoPlayerController

video_player插件成功安裝且權限設置完成後,需要創建一個VideoPlayerControllerVideoPlayerController類允許你播放不同類型的影片並進行播放控制,在播放影片前,需要對播放控制器進行初始化。初始化過程主要是與影片源建立連接和播放控制的準備

  1. 創建一個StatefulWidget組件和State

  2. State類中增加一個變量來存放VideoPlayerController

  3. State類中增加另外一個變量來存放VideoPlayerController.initialize返回的Future

  4. initState方法裡創建和初始化控制器

  5. dispose方法裡銷毀控制器

class VideoPlayerScreen extends StatefulWidget {
  VideoPlayerScreen({Key key}) : super(key: key);

  @override
  _VideoPlayerScreenState createState() => _VideoPlayerScreenState();
}

class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
  VideoPlayerController _controller;
  Future<void> _initializeVideoPlayerFuture;

  @override
  void initState() {
    // Create an store the VideoPlayerController. The VideoPlayerController
    // offers several different constructors to play videos from assets, files,
    // or the internet.
    _controller = VideoPlayerController.network(
      'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
    );

    _initializeVideoPlayerFuture = _controller.initialize();

    super.initState();
  }

  @override
  void dispose() {
    // Ensure disposing of the VideoPlayerController to free up resources.
    _controller.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // Complete the code in the next step.
  }
}

影片播放器

video_player插件提供了VideoPlayer組件來展示已經被VideoPlayerController初始化完成的影片。默認情況下,VideoPlayer組件會盡可能撐滿整個空間。但是這通常不會太理想,因為很多時候影片需要在特定的寬高比下展示,比如16x9或者4x3

因此,你可以把VideoPlayer組件嵌進一個 AspectRatio組件中,保證影片播放保持正確的比例,此外,你必須在_initializeVideoPlayerFuture完成後才展示VideoPlayer組件。你可以使用FutureBuilder來展示一個旋轉的加載圖標直到初始化完成。請注意:控制器初始化完成並不會立即開始播放

// Use a FutureBuilder to display a loading spinner while waiting for the
// VideoPlayerController to finish initializing.
FutureBuilder(
  future: _initializeVideoPlayerFuture,
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.done) {
      // If the VideoPlayerController has finished initialization, use
      // the data it provides to limit the aspect ratio of the VideoPlayer.
      return AspectRatio(
        aspectRatio: _controller.value.aspectRatio,
        // Use the VideoPlayer widget to display the video.
        child: VideoPlayer(_controller),
      );
    } else {
      // If the VideoPlayerController is still initializing, show a
      // loading spinner.
      return Center(child: CircularProgressIndicator());
    }
  },
)

播放視頻和暫停視頻

默認情況下,播放器啟動時會處於暫停狀態。需要調用VideoPlayerController提供的play()方法來開始播放,然後需要調用pause()方法來停止播放

此範例中加入了一個FloatingActionButton,這個按鈕會根據播放狀態展示播放或者暫停的圖標。當使用者點擊按鈕,會切換播放狀態,如果當前是暫停狀態,就開始播放。如果當前是播放狀態,就暫停播放

FloatingActionButton(
  onPressed: () {
    // Wrap the play or pause in a call to `setState`. This ensures the
    // correct icon is shown
    setState(() {
      // If the video is playing, pause it.
      if (_controller.value.isPlaying) {
        _controller.pause();
      } else {
        // If the video is paused, play it.
        _controller.play();
      }
    });
  },
  // Display the correct icon depending on the state of the player.
  child: Icon(
    _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
  ),
)

完整程式碼範例:

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';

void main() => runApp(VideoPlayerApp());

class VideoPlayerApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Video Player Demo',
      home: VideoPlayerScreen(),
    );
  }
}

class VideoPlayerScreen extends StatefulWidget {
  VideoPlayerScreen({Key key}) : super(key: key);

  @override
  _VideoPlayerScreenState createState() => _VideoPlayerScreenState();
}

class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
  VideoPlayerController _controller;
  Future<void> _initializeVideoPlayerFuture;

  @override
  void initState() {
    // Create and store the VideoPlayerController. The VideoPlayerController
    // offers several different constructors to play videos from assets, files,
    // or the internet.
    _controller = VideoPlayerController.network(
      'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
    );

    // Initialize the controller and store the Future for later use.
    _initializeVideoPlayerFuture = _controller.initialize();

    // Use the controller to loop the video.
    _controller.setLooping(true);

    super.initState();
  }

  @override
  void dispose() {
    // Ensure disposing of the VideoPlayerController to free up resources.
    _controller.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Butterfly Video'),
      ),
      // Use a FutureBuilder to display a loading spinner while waiting for the
      // VideoPlayerController to finish initializing.
      body: FutureBuilder(
        future: _initializeVideoPlayerFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            // If the VideoPlayerController has finished initialization, use
            // the data it provides to limit the aspect ratio of the video.
            return AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              // Use the VideoPlayer widget to display the video.
              child: VideoPlayer(_controller),
            );
          } else {
            // If the VideoPlayerController is still initializing, show a
            // loading spinner.
            return Center(child: CircularProgressIndicator());
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // Wrap the play or pause in a call to `setState`. This ensures the
          // correct icon is shown.
          setState(() {
            // If the video is playing, pause it.
            if (_controller.value.isPlaying) {
              _controller.pause();
            } else {
              // If the video is paused, play it.
              _controller.play();
            }
          });
        },
        // Display the correct icon depending on the state of the player.
        child: Icon(
          _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

上一篇
Day29 Flutter Persistence
系列文
從零開始的Flutter世界30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言