中堅プログラマーの備忘録

忘れっぽくなってきたので備忘録として・・・

【C#.net】PictureBox、AxWindowsMediaPlayerのメモリ解放について

1.概要

画像、動画のViewerを作成していた時に
メモリが徐々に増加していることの気づきました。
自分ではしっかりとメモリの解放を行っているつもりでも
実は出来ていなかった、なんてことはプログラマーにとっては
日常茶飯事ではないでしょうか?
特に、画像や動画などは多くのメモリを使うため
メモリリークがわかりやすいです。
今回は画像、動画のViewerとして一般的に使われることが多いであろう
【PictureBox】、【AxWindowsMediaPlayer】
について対策をしてみます。

2.PictureBoxの挙動について

まずは読み込みについては下記のとおり。
※FormにPictureBoxコントロールがあることを想定しています。

System.IO.FileStream fs1 = new System.IO.FileStream("画像のパス", System.IO.FileMode.Open, System.IO.FileAccess.Read);
pictureBox1.Image = System.Drawing.Image.FromStream(fs1);
fs1.Close();
fs1.Dispose();
pictureBox1.Update();


これで画像が表示されます。
次に画像を切り替える場合は、普通に考えると上記スクリプトを繰り返すことになります。
ですがこれをやってしまうと切り替え前のデータがメモリ上に残り続けてしまいます。
その為、次の画像を読み込む前に下記のスクリプトを実行します。

pictureBox1.Image.Dispose();
pictureBox1.Image = null;


これでリソースが開放されます。

3.AxWindowsMediaPlayerの挙動について

Formアプリケーションで動画再生を行うには下記の方法があります。
①WindowsMediaPlayerコントロール
②DirectShow
③MediaFoundation
※上記以外にも存在しますがここでは省きます。


正直言って【DirectShow】、【MediaFoundation】については
実際に扱ってみた感想として、難易度が高いと感じました。
また、コーデックが別途必要になったりと、よっぽど独自のプレイヤーを作成したい
という志がない限り、扱うのはお勧めできません。


逆に【WindowsMediaPlayerコントロール】であれば
難易度も低く、コーデックもローカルにインストールされている
【WindowsMediaPlayer】に依存するので
簡単に動画を再生してみたいといったニーズに対しては
これがマッチしているのではないかと思います。


と言いつつも、メモリの解放について非常に難があります。


今回は私が経験した内容について記述させて頂きます。
方法として正解かどうかは別として、実際にメモリリークが改善した
といった内容になりますので、扱いには注意が必要になります。

アプリケーション概要

ローカルに複数存在するmp4ファイルを順番に再生していく

不具合内容

動画再生が完了し、次の動画を再生開始するたびにメモリが徐々に増えていく。
wmvファイルは問題なくmp4ファイルの時にのみ起こる。

対策

【WindowsMediaPlayerコントロール】を再生が完了するたびに
解放し、新たにインスタンス作成を行う。
但し、Disposeしただけでは
COM オブジェクトへのマネージ参照を解放しきれないという現象が発生したため
【Marshal.FinalReleaseComObject】で怪しい箇所を開放していった。

4.スクリプト

※コントロールにTimerを貼り付けています。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace MediaPlayerTest
{
    public partial class Form1 : Form
    {
        private AxWMPLib.AxWindowsMediaPlayer mediaPlayer;
        private string[] playList;
        private int LIST_NUM;
        private int nowPlayNum = 0;         //  現在の再生番号
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            // プレイリストを作成する
            var tmpList = new List<string>();
            string[] tmpStr;
            string[] patterns = { ".mp4", ".wmv", ".avi" };

            tmpStr = System.IO.Directory.GetFiles(@"C:\test", "*.*");       // C:\test内にある動画を順次再生する
            var tmpString = tmpStr.Where(file => patterns.Any(pattern => file.ToLower().EndsWith(pattern)));
            foreach (string tmp in tmpString)
            {
                tmpList.Add(tmp);
            }
            playList = tmpList.ToArray();
            LIST_NUM = playList.Length;
            // 再生開始
            MakeMediaPlayer();
        }

        /// <summary>
        /// ステータスが変更された時に呼び出される
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MediaPlayer_PlayStateChange(object sender, AxWMPLib._WMPOCXEvents_PlayStateChangeEvent e)
        {
            switch (e.newState)
            {
                case (int)WMPLib.WMPPlayState.wmppsStopped:
                    //停止時
                    break;

                case (int)WMPLib.WMPPlayState.wmppsPlaying:
                    //再生時

                    break;

                case (int)WMPLib.WMPPlayState.wmppsPaused:
                    // 一時停止

                    break;

                case (int)WMPLib.WMPPlayState.wmppsMediaEnded:
                    //再生終了時
                    timer1.Start();
                    break;

                case (int)WMPLib.WMPPlayState.wmppsTransitioning:
                    //再生準備中

                    break;

                case (int)WMPLib.WMPPlayState.wmppsReady:
                    //再生準備完了

                    break;

                default:
                    break;
            }
        }

        /// <summary>
        /// MediaPlayerの作成
        /// </summary>
        private void MakeMediaPlayer()
        {
            if (mediaPlayer == null)
            {
                mediaPlayer = new AxWMPLib.AxWindowsMediaPlayer();
                this.Controls.Add(mediaPlayer);
                mediaPlayer.stretchToFit = true;
                mediaPlayer.uiMode = "none";               // UIを消す
                mediaPlayer.Location = new Point(0, 0);
                mediaPlayer.Dock = DockStyle.Fill;
                mediaPlayer.Size = new Size(640, 360);
                mediaPlayer.settings.autoStart = false;
                mediaPlayer.Ctlenabled = false;           // ダブルクリックによるフルスクリーン出力を無効化
                mediaPlayer.enableContextMenu = false;    // 右クリックによるコンテキストメニューの出力を無効化
                mediaPlayer.PlayStateChange += MediaPlayer_PlayStateChange;

                ((System.ComponentModel.ISupportInitialize)(mediaPlayer)).EndInit();
                mediaPlayer.URL = playList[nowPlayNum];
                mediaPlayer.Ctlcontrols.play();
            }
        }

        /// <summary>
        /// MediaPlayerの解放
        /// </summary>
        private void DisposeMediaPlayer()
        {
            mediaPlayer.URL = "";
            mediaPlayer.close();
            mediaPlayer.PlayStateChange -= MediaPlayer_PlayStateChange;
            mediaPlayer.Controls.Clear();

            WMPLib.IWMPMedia item = mediaPlayer.currentPlaylist.Item[0];
            mediaPlayer.currentPlaylist.removeItem(item);
            Marshal.FinalReleaseComObject(item);
            item = null;

            Marshal.FinalReleaseComObject(mediaPlayer.settings);
            Marshal.FinalReleaseComObject(mediaPlayer.Ctlcontrols);
            Marshal.FinalReleaseComObject(mediaPlayer.currentPlaylist);

            Marshal.FinalReleaseComObject(mediaPlayer.mediaCollection);
            Marshal.FinalReleaseComObject(mediaPlayer.playlistCollection);
            Marshal.FinalReleaseComObject(mediaPlayer.Error);
            Marshal.FinalReleaseComObject(mediaPlayer.cdromCollection);
            Marshal.FinalReleaseComObject(mediaPlayer.closedCaption);
            Marshal.FinalReleaseComObject(mediaPlayer.dvd);
            Marshal.FinalReleaseComObject(mediaPlayer.network);

            mediaPlayer.Dispose();
            mediaPlayer = null;
            this.Controls.Remove(mediaPlayer);
        }

        /// <summary>
        /// 再生番号更新
        /// </summary>
        private void playNumIncrement()
        {
            nowPlayNum++;
            if (nowPlayNum >= LIST_NUM)
            {
                nowPlayNum = 0;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void timer1_Tick(object sender, EventArgs e)
        {
            timer1.Stop();
            DisposeMediaPlayer();
            playNumIncrement();
            MakeMediaPlayer();
        }
    }
}

5.まとめ

ただ動画を再生するだけなのですが
非常に面倒なスクリプトになってしまいました。
リソースの解放の処理は全て必要かどうか把握できていませんが
出来ることは全部やってしまおう、という感じで処理しています。

また、【Windows 10 Enterprise LTSB 2016】では
どうしてもメモリリークが解決できませんでした。

COMなのでこれ以上はもうどうしようもないと判断し
これについてはあきらめました。