diff --git a/BizHawk.Client.Common/BizHawk.Client.Common.csproj b/BizHawk.Client.Common/BizHawk.Client.Common.csproj
index 8eba8e2c9d..53ed41ea9c 100644
--- a/BizHawk.Client.Common/BizHawk.Client.Common.csproj
+++ b/BizHawk.Client.Common/BizHawk.Client.Common.csproj
@@ -180,6 +180,8 @@
     </Compile>
     <Compile Include="movie\conversions\MovieConversionExtensions.cs" />
     <Compile Include="movie\HeaderKeys.cs" />
+    <Compile Include="movie\import\Fm2Import.cs" />
+    <Compile Include="movie\import\IMovieImport.cs" />
     <Compile Include="movie\import\MovieImport.cs" />
     <Compile Include="movie\import\PJMImport.cs" />
     <Compile Include="movie\interfaces\ILogEntryGenerator.cs" />
diff --git a/BizHawk.Client.Common/movie/import/Fm2Import.cs b/BizHawk.Client.Common/movie/import/Fm2Import.cs
new file mode 100644
index 0000000000..22b472762c
--- /dev/null
+++ b/BizHawk.Client.Common/movie/import/Fm2Import.cs
@@ -0,0 +1,197 @@
+using System;
+using System.IO;
+
+using BizHawk.Common;
+using BizHawk.Common.BufferExtensions;
+
+namespace BizHawk.Client.Common
+{
+	[ImportExtension(".fm2")]
+	public class Fm2Import : MovieImporter
+	{
+		protected override void RunImport()
+		{
+			var emulator = "FCEUX";
+			var platform = "NES"; // TODO: FDS?
+
+			Result.Movie.HeaderEntries[HeaderKeys.PLATFORM] = platform;
+
+			using (var sr = SourceFile.OpenText())
+			{
+				string line;
+				int lineNum = 0;
+
+				while ((line = sr.ReadLine()) != null)
+				{
+					lineNum++;
+
+					if (line == string.Empty)
+					{
+						continue;
+					}
+					else if (line[0] == '|')
+					{
+						// TODO: import a frame of input
+						// TODO: report any errors importing this frame and bail out if so
+					}
+					else if (line.ToLower().StartsWith("sub"))
+					{
+						var subtitle = ImportTextSubtitle(line);
+
+						if (!string.IsNullOrEmpty(subtitle))
+						{
+							Result.Movie.Subtitles.AddFromString(subtitle);
+						}
+					}
+					else if (line.ToLower().StartsWith("emuversion"))
+					{
+						Result.Movie.Comments.Add(
+							string.Format("{0} {1} version {2}", EMULATIONORIGIN, emulator, ParseHeader(line, "emuVersion"))
+						);
+					}
+					else if (line.ToLower().StartsWith("version"))
+					{
+						string version = ParseHeader(line, "version");
+
+						if (version != "3")
+						{
+							Result.Warnings.Add("Detected a .fm2 movie version other than 3, which is unsupported");
+						}
+						else
+						{
+							Result.Movie.Comments.Add(MOVIEORIGIN + " .fm2 version 3");
+						}
+					}
+					else if (line.ToLower().StartsWith("romfilename"))
+					{
+						Result.Movie.HeaderEntries[HeaderKeys.GAMENAME] = ParseHeader(line, "romFilename");
+					}
+					else if (line.ToLower().StartsWith("cdgamename"))
+					{
+						Result.Movie.HeaderEntries[HeaderKeys.GAMENAME] = ParseHeader(line, "cdGameName");
+					}
+					else if (line.ToLower().StartsWith("romchecksum"))
+					{
+						string blob = ParseHeader(line, "romChecksum");
+						byte[] md5 = DecodeBlob(blob);
+						if (md5 != null && md5.Length == 16)
+						{
+							Result.Movie.HeaderEntries[MD5] = md5.BytesToHexString().ToLower();
+						}
+						else
+						{
+							Result.Warnings.Add("Bad ROM checksum.");
+						}
+					}
+					else if (line.ToLower().StartsWith("comment author"))
+					{
+						Result.Movie.HeaderEntries[HeaderKeys.AUTHOR] = ParseHeader(line, "comment author");
+					}
+					else if (line.ToLower().StartsWith("rerecordcount"))
+					{
+						int rerecordCount = 0;
+						int.TryParse(ParseHeader(line, "rerecordCount"), out rerecordCount);
+
+						Result.Movie.Rerecords = (ulong)rerecordCount;
+					}
+					else if (line.ToLower().StartsWith("guid"))
+					{
+						continue; //We no longer care to keep this info
+					}
+					else if (line.ToLower().StartsWith("startsfromsavestate"))
+					{
+						// If this movie starts from a savestate, we can't support it.
+						if (ParseHeader(line, "StartsFromSavestate") == "1")
+						{
+							Result.Errors.Add("Movies that begin with a savestate are not supported.");
+							break;
+						}
+					}
+					else if (line.ToLower().StartsWith("palflag"))
+					{
+						Result.Movie.HeaderEntries[HeaderKeys.PAL] = ParseHeader(line, "palFlag");
+					}
+					else if (line.ToLower().StartsWith("fourscore"))
+					{
+						bool fourscore = (ParseHeader(line, "fourscore") == "1");
+						if (fourscore)
+						{
+							// TODO: set controller config sync settings
+						}
+					}
+					else
+					{
+						Result.Movie.Comments.Add(line); // Everything not explicitly defined is treated as a comment.
+					}
+				}
+			}
+		}
+
+		private static string ImportTextSubtitle(string line)
+		{
+			line = SingleSpaces(line);
+
+			// The header name, frame, and message are separated by whitespace.
+			int first = line.IndexOf(' ');
+			int second = line.IndexOf(' ', first + 1);
+			if (first != -1 && second != -1)
+			{
+				// Concatenate the frame and message with default values for the additional fields.
+				string frame = line.Substring(0, first);
+				string length = line.Substring(first + 1, second - first - 1);
+				string message = line.Substring(second + 1).Trim();
+
+				return "subtitle " + frame + " 0 0 " + length + " FFFFFFFF " + message;
+			}
+
+			return null;
+		}
+
+		// Reduce all whitespace to single spaces.
+		private static string SingleSpaces(string line)
+		{
+			line = line.Replace("\t", " ");
+			line = line.Replace("\n", " ");
+			line = line.Replace("\r", " ");
+			line = line.Replace("\r\n", " ");
+			string prev;
+			do
+			{
+				prev = line;
+				line = line.Replace("  ", " ");
+			}
+			while (prev != line);
+			return line;
+		}
+
+		// Decode a blob used in FM2 (base64:..., 0x123456...)
+		private static byte[] DecodeBlob(string blob)
+		{
+			if (blob.Length < 2)
+			{
+				return null;
+			}
+			if (blob[0] == '0' && (blob[1] == 'x' || blob[1] == 'X'))
+			{
+				// hex
+				return Util.HexStringToBytes(blob.Substring(2));
+			}
+			else
+			{
+				// base64
+				if (!blob.ToLower().StartsWith("base64:"))
+				{
+					return null;
+				}
+				try
+				{
+					return Convert.FromBase64String(blob.Substring(7));
+				}
+				catch (FormatException)
+				{
+					return null;
+				}
+			}
+		}
+	}
+}
diff --git a/BizHawk.Client.Common/movie/import/IMovieImport.cs b/BizHawk.Client.Common/movie/import/IMovieImport.cs
new file mode 100644
index 0000000000..bea7525307
--- /dev/null
+++ b/BizHawk.Client.Common/movie/import/IMovieImport.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace BizHawk.Client.Common
+{
+	public interface IMovieImport
+	{
+		ImportResult Import(string path);
+	}
+
+	public abstract class MovieImporter : IMovieImport
+	{
+		public const string COMMENT = "comment";
+		public const string COREORIGIN = "CoreOrigin";
+		public const string CRC16 = "CRC16";
+		public const string CRC32 = "CRC32";
+		public const string EMULATIONORIGIN = "emuOrigin";
+		public const string GAMECODE = "GameCode";
+		public const string INTERNALCHECKSUM = "InternalChecksum";
+		public const string JAPAN = "Japan";
+		public const string MD5 = "MD5";
+		public const string MOVIEORIGIN = "MovieOrigin";
+		public const string PORT1 = "port1";
+		public const string PORT2 = "port2";
+		public const string PROJECTID = "ProjectID";
+		public const string SHA256 = "SHA256";
+		public const string SUPERGAMEBOYMODE = "SuperGameBoyMode";
+		public const string STARTSECOND = "StartSecond";
+		public const string STARTSUBSECOND = "StartSubSecond";
+		public const string SYNCHACK = "SyncHack";
+		public const string UNITCODE = "UnitCode";
+
+		public ImportResult Import(string path)
+		{
+			SourceFile = new FileInfo(path);
+
+			if (!SourceFile.Exists)
+			{
+				Result.Errors.Add(string.Format("Could not find the file {0}", path));
+				return Result;
+			}
+
+			var newFileName = SourceFile.FullName + "." + Bk2Movie.Extension;
+			Result.Movie = new Bk2Movie(newFileName);
+
+			RunImport();
+
+			return Result;
+		}
+
+
+		protected ImportResult Result = new ImportResult();
+
+		protected FileInfo SourceFile;
+
+		protected abstract void RunImport();
+
+		// Get the content for a particular header.
+		protected static string ParseHeader(string line, string headerName)
+		{
+			// Case-insensitive search.
+			int x = line.ToLower().LastIndexOf(
+				headerName.ToLower()
+			) + headerName.Length;
+			string str = line.Substring(x + 1, line.Length - x - 1);
+			return str.Trim();
+		}
+	}
+
+	public class ImportResult
+	{
+		public ImportResult()
+		{
+			Warnings = new List<string>();
+			Errors = new List<string>();
+		}
+
+		public IList<string> Warnings { get; private set; }
+		public IList<string> Errors { get; private set; }
+
+		public Bk2Movie Movie { get; set; }
+	}
+
+	[AttributeUsage(AttributeTargets.Class)]
+	public class ImportExtension : Attribute
+	{
+		public ImportExtension(string extension)
+		{
+			Extension = extension;
+		}
+
+		public string Extension { get; private set; }
+	}
+}
diff --git a/BizHawk.Client.Common/movie/import/MovieImport.cs b/BizHawk.Client.Common/movie/import/MovieImport.cs
index edac187faf..8d008efd7f 100644
--- a/BizHawk.Client.Common/movie/import/MovieImport.cs
+++ b/BizHawk.Client.Common/movie/import/MovieImport.cs
@@ -73,9 +73,21 @@ namespace BizHawk.Client.Common
 			warningMsg = string.Empty;
 			string ext = path != null ? Path.GetExtension(path).ToUpper() : string.Empty;
 
+			// TODO: reflect off the assembly and find an IMovieImporter with the appropriate ImportExtension metadata
+			if (ext == ".FM2")
+			{
+				var result = new Fm2Import().Import(path);
+				errorMsg = result.Errors.First();
+				warningMsg = result.Errors.First();
+				return result.Movie;
+			}
+
 			if (ext == ".PJM")
 			{
-				return PJMImport.Import(path, out errorMsg, out warningMsg);
+				var result = new PJMImport().Import(path);
+				errorMsg = result.Errors.First();
+				warningMsg = result.Errors.First();
+				return result.Movie;
 			}
 
 			BkmMovie m = new BkmMovie();
diff --git a/BizHawk.Client.Common/movie/import/PJMImport.cs b/BizHawk.Client.Common/movie/import/PJMImport.cs
index 57b0a1effd..342a12d492 100644
--- a/BizHawk.Client.Common/movie/import/PJMImport.cs
+++ b/BizHawk.Client.Common/movie/import/PJMImport.cs
@@ -6,13 +6,12 @@ using System.Text;
 
 namespace BizHawk.Client.Common
 {
-	public static class PJMImport
+	[ImportExtension(".pjm")]
+	public class PJMImport : MovieImporter
 	{
-		public static Bk2Movie Import(string path, out string errorMsg, out string warningMsg)
+		protected override void RunImport()
 		{
-			errorMsg = string.Empty;
-			warningMsg = string.Empty;
-			return new Bk2Movie();
+			// TODO
 		}
 	}
 }