I recently mentioned .NET assembly patching as a way to tweak Windows Media Center and fix broken things or change how specific features work internally.
It's definitely not something new and I've personally been using this approach for years to implement QoL improvements that make my everyday use of WMC a more pleasant experience. But since it requires replacing the Windows Media Center files by patched versions, it was a risky option when Windows Media Center was still supported (as there was a risk the patched .dlls would end up being overwritten by Microsoft during an update).
Now that Windows 8.1 - the latest Windows version offering WMC as a paid option - is no longer supported, this risk is pretty much non-existent.
As I suspect some of these patches might be useful to other people, I decided to share them and distribute the patched assemblies so that people not comfortable with this technique can benefit from these QoL improvements.
Given these changes target very specific areas of WMC, I decided to call this project "WMC mini-mods"
For those who are interested in the gnarly details, here's the list of opt-in patches I decided to include in the 1.0.0 version:
mcepg.dll patches:
Option to disable the automatic removal of "invalid" TV recordings:
By default, when launching ehshell.exe, a "watcher" instance is automatically started by WMC to scan new TV recordings and delete TV recordings that are thought to be missing or unreadable. It's a very convenient feature as it ensures TV recordings that are no longer on the disk are automatically removed from the WMC database.
Sadly, this feature has a huge downside when using remote shares: if a remote share is for some reason inaccessible at the exact moment WMC starts scanning it (e.g the server is being rebooted or the WMC machine is not connected to the network), all the recordings it contains are automatically removed from the WMC database. It's of course no big deal when you have a few hundred recordings but - if like me - you have thousands of recordings, the user experience is absolutely horrible, as WMC has to rescan everything, which takes ages. Not to mention that the thumbnails are deleted and have to be recreated one by one by the ehvid.exe process (which is quite unreliable when having to handle thousands of recordings at the same time).
The workaround for that is quite simple: disabling the automatic removal so that recordings are not deleted by WMC, unless when they are deleted via the WMC GUI or from the disk when ehshell.exe is running.
For that, I patched the Watcher.EnsureValidRecordings() method to return immediately if DisableInvalidRecordingsDeletion was set to 1 in the registry:
Code: Select all
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.MediaCenter.Interop;
namespace Microsoft.MediaCenter.Pvr
{
// Token: 0x020001F7 RID: 503
public sealed partial class Watcher : ICollection<string>, IEnumerable<string>, IEnumerable, IDisposable
{
// Token: 0x06000F9C RID: 3996 RVA: 0x000411B0 File Offset: 0x0003F3B0
private void EnsureValidRecordings()
{
var setting = Settings.Recording.Value<int>("DisableInvalidRecordingsDeletion");
if (setting.Value == 1)
{
return;
}
using (ILockHandle usingHandle = this._library.UsingHandle)
{
if (usingHandle.IsLocked)
{
foreach (object obj in this._library.Recordings)
{
Recording recording = (Recording)obj;
if (!this.Contains(recording.FileName))
{
this.OnDeletedFile(recording.FileName);
}
else
{
Win32Api.WIN32_FILE_ATTRIBUTE_DATA win32_FILE_ATTRIBUTE_DATA = default(Win32Api.WIN32_FILE_ATTRIBUTE_DATA);
if (!Win32Api.GetFileAttributesEx(recording.FileName, Win32Api.GET_FILEEX_INFO_LEVELS.GetFileExInfoStandard, out win32_FILE_ATTRIBUTE_DATA))
{
int lastWin32Error = Marshal.GetLastWin32Error();
if ((long)lastWin32Error != 5L && (long)lastWin32Error != 86L && (long)lastWin32Error != 1244L && (long)lastWin32Error != 1326L)
{
this.OnDeletedFile(recording.FileName);
}
}
}
}
}
}
}
}
}
For reasons beyond my comprehension, Windows Media Center doesn't store the episode number or season number in the .wtv files it creates, even if these details were available in the EPG. Of course, one of the common approaches to deal with that is to store these details in the program description, but if your EPG source doesn't do that, the episode details will be lost. Yet, these details can be very useful when creating your own library management tools.
The fix for that is very simple: storing these values in the WTV metadata when creating the recordings. For that, I patched the RecordingMetadata.WriteInitialMetadata() method to store the episode and season numbers as WM/EpisodeNumber and WM/SeasonNumber attributes if WriteAdditionalMetadata was set to 1 in the registry.
Code: Select all
using System;
namespace Microsoft.MediaCenter.Pvr
{
// Token: 0x02000189 RID: 393
internal static partial class RecordingMetadata
{
// Token: 0x06000BBD RID: 3005 RVA: 0x0002EEE8 File Offset: 0x0002D0E8
public static void WriteInitialMetadata(IRecordingMetadata metadata, RecordingAttributes attributes)
{
if (metadata == null)
{
throw new ArgumentNullException("metadata");
}
if (attributes == null)
{
throw new ArgumentNullException("attributes");
}
attributes.Write("WM/MediaClassPrimaryID", RecordingMetadata.PrimaryCLSID);
attributes.Write("WM/MediaClassSecondaryID", RecordingMetadata.SecondaryCLSID);
attributes.Write("Title", metadata.Title);
attributes.Write("WM/SubTitle", metadata.EpisodeTitle);
attributes.Write("WM/SubTitleDescription", metadata.Description);
attributes.Write("WM/Genre", metadata.Genre);
attributes.Write("WM/OriginalReleaseTime", metadata.YearString);
attributes.Write("WM/Language", metadata.Language);
attributes.Write("WM/MediaCredits", metadata.Credits);
attributes.Write("WM/MediaStationCallSign", metadata.CallSign);
attributes.Write("WM/MediaStationName", metadata.StationName);
attributes.Write("WM/MediaNetworkAffiliation", metadata.NetworkAffiliation);
attributes.Write("WM/MediaOriginalChannel", metadata.Channel);
attributes.Write("WM/MediaOriginalChannelSubNumber", metadata.ChannelSubNumber);
attributes.Write("WM/MediaOriginalBroadcastDateTime", metadata.OriginalAirString);
attributes.Write("WM/MediaOriginalRunTime", metadata.RunTime);
attributes.Write("WM/MediaIsStereo", metadata.Stereo);
attributes.Write("WM/MediaIsRepeat", metadata.Repeat);
attributes.Write("WM/MediaIsLive", metadata.Live);
attributes.Write("WM/MediaIsTape", metadata.Tape);
attributes.Write("WM/MediaIsDelay", metadata.Delay);
attributes.Write("WM/MediaIsSubtitled", metadata.SubTitled);
attributes.Write("WM/MediaIsMovie", metadata.Movie);
attributes.Write("WM/MediaIsPremiere", metadata.Premiere);
attributes.Write("WM/MediaIsFinale", metadata.Finale);
attributes.Write("WM/MediaIsSAP", metadata.SAP);
attributes.Write("WM/MediaIsSport", metadata.Sport);
attributes.Write("WM/ParentalRating", metadata.RatingLevel);
attributes.Write("WM/ParentalRatingReason", metadata.RatingReason);
attributes.Write("WM/Provider", metadata.Provider);
attributes.Write("WM/ProviderCopyright", metadata.Copyright);
attributes.Write("WM/ProviderRating", metadata.StarRating);
attributes.Write("WM/VideoClosedCaptioning", metadata.CC);
attributes.Write("WM/WMRVEncodeTime", metadata.StartTimeTicks);
attributes.Write("WM/WMRVSeriesUID", metadata.SeriesUID);
attributes.Write("WM/WMRVServiceID", metadata.ServiceID);
attributes.Write("WM/WMRVProgramID", metadata.ProgramID);
attributes.Write("WM/WMRVRequestID", metadata.RecordingRequestID);
attributes.Write("WM/WMRVScheduleItemID", metadata.Id);
attributes.Write("WM/WMRVQuality", metadata.Quality);
attributes.Write("WM/WMRVOriginalSoftPrePadding", metadata.OriginalSoftPrePadding);
attributes.Write("WM/WMRVOriginalSoftPostPadding", metadata.OriginalSoftPostPadding);
attributes.Write("WM/WMRVHardPrePadding", metadata.HardPrePadding);
attributes.Write("WM/WMRVHardPostPadding", metadata.HardPostPadding);
attributes.Write("WM/WMRVBrandingName", metadata.BrandingName);
attributes.Write("WM/WMRVBrandingImageID", metadata.BrandingImageID);
attributes.Write("WM/WMRVATSCContent", metadata.ATSCContent);
attributes.Write("WM/WMRVDTVContent", metadata.DTV);
attributes.Write("WM/WMRVHDContent", metadata.HDTV);
var setting = Settings.Recording.Value<int>("WriteAdditionalMetadata");
if (setting.Value == 1)
{
if (metadata is Recording recording)
{
attributes.Write("WM/SeasonNumber", recording.Program.SeasonNumber);
attributes.Write("WM/EpisodeNumber", recording.Program.EpisodeNumber);
}
else if (metadata is RemoteRecording remoteRecording)
{
attributes.Write("WM/SeasonNumber", remoteRecording.Program.SeasonNumber);
attributes.Write("WM/EpisodeNumber", remoteRecording.Program.EpisodeNumber);
}
}
}
}
}
Option to disable TV overscan independently of the configured TV mode:
While most users have probably never realized it, Windows Media Center automatically applies a small overscan to all TV programs (live or recorded). Some people - myself included - didn't like that and eventually found a workaround: running the display configuration wizard and selecting "TV set" as the main display to disable it (in this case, WMC uses what it calls a "TV skin" mode that assumes the TV set itself will perform an overscan and disables its own one to prevent a double-overscan). Sadly, this had side effects (e.g it slightly changed the colors and contrast) and required tweaking a few registry keys to undo the extended margins applied by WMC when using this mode.
To simplify things a lot, I patched PresentationPolicy.OverscanMode to completely disable the overscan when DisableVideoOverscan is set to 1, without ever having to run the display configuration wizard.
Code: Select all
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.MediaCenter.Interop;
using Microsoft.MediaCenter.Shell;
using Microsoft.MediaCenter.UI;
using Microsoft.MediaCenter.UI.Drawing;
using Microsoft.MediaCenter.UI.VideoPlayback;
using ServiceBus.UIFramework;
using Microsoft.Win32;
namespace Microsoft.MediaCenter.Playback
{
// Token: 0x020008C6 RID: 2246
public partial class PresentationPolicy : DynamicImage.IDynamicImageOwner, IDisposable
{
// Token: 0x17001481 RID: 5249
// (get) Token: 0x06004F7B RID: 20347 RVA: 0x001425A4 File Offset: 0x001407A4
private VideoOverscanMode OverscanMode
{
get
{
var key = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Media Center\\Settings\\DisplayService");
if (UCPUtility.CheckBoolRegVal(key, "DisableVideoOverscan"))
{
return VideoOverscanMode.NoOverscan;
}
VideoOverscanMode videoOverscanMode = VideoOverscanMode.NoOverscan;
if (!UCPUtility.UseTVSkin || this.Form.WindowState != FormWindowState.Maximized)
{
if (this.HasNoOverscanOverride)
{
videoOverscanMode = VideoOverscanMode.ValidContent;
}
else if (this.IsBroadcastMediaItemPlaying)
{
videoOverscanMode = VideoOverscanMode.InvalidContent;
}
else if (this.IsDvdMediaItemPlaying)
{
videoOverscanMode = VideoOverscanMode.ValidContent;
}
}
return videoOverscanMode;
}
}
}
}
I recently had to add this one to work around an issue I was seeing on an Intel NUC. While it worked for me, it seems it's not enough to fix the issues encountered by owners of Intel 11th generation iGPUs: that said, I decided to keep it in case it could help other people. For that, I had to patch the PageBasedUCPService.OnRendererConnected() method:
Code: Select all
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel.Design;
using System.Runtime.InteropServices;
using System.Threading;
using ehiExtCOM;
using EHMedia;
using MediaCenter.Extensibility;
using Microsoft.MediaCenter.Debug;
using Microsoft.MediaCenter.Hosting.Infrastructure;
using Microsoft.MediaCenter.Interop;
using Microsoft.MediaCenter.Playback;
using Microsoft.MediaCenter.Shell;
using Microsoft.MediaCenter.UI;
using Microsoft.MediaCenter.UI.Drawing;
using Microsoft.MediaCenter.UI.VideoPlayback;
using Microsoft.MediaCenter.Utility;
using Microsoft.Win32;
namespace ServiceBus.UIFramework
{
// Token: 0x02000B4F RID: 2895
internal sealed partial class PageBasedUCPService : UCPService, IUsageLogger, IUIVideoStreamBroker
{
// Token: 0x060066D7 RID: 26327 RVA: 0x001A2C64 File Offset: 0x001A0E64
private void OnRendererConnected(object sender, EventArgs args)
{
PerfInfo.SetMark("ehShell::OnRendererEnter");
Capabilities.UpdateRemoteKeyboard();
this.AddPrivateFonts();
switch (this._session.DeviceType)
{
case DeviceType.Gdi:
this.InitGdiDevice();
break;
case DeviceType.NtDirectX9:
this.InitNtDevice();
break;
case DeviceType.XeDirectX9:
this.InitXeDevice();
break;
}
if (Capabilities.IsUISessionRemoted)
{
PageBasedUCPService.s_hShellReadyEvent = Win32Api.OpenEvent(2U, false, "ShellDesktopSwitchEvent");
PageBasedUCPService.s_hUserRequestedCloseEvent = Win32Api.CreateEvent(IntPtr.Zero, false, false, "UserRequestedEhshellCloseEvent");
}
Dx9DeviceInfo.RemoveFromRegistry(Registry.CurrentUser, "Software\\Microsoft\\Windows\\CurrentVersion\\Media Center\\Dx9DeviceInfo");
this._session.InvalidKeyFocus += this.OnInvalidKeyFocus;
this._session.KeyCoalescePolicy = new KeyCoalesceFilter(this.QueryCoalesce);
this._popupUiTracker = new VisibleUITracker();
this._popupUiTracker.MemberStatusChange += this.OnVisiblePopupChange;
this._session.OnUnhandledException += this.MyUnhandledExceptionHandler;
this._session.Rendering.OnRendererSuspended += new RenderingSession.RendererSuspendedHandler(this.MyRendererSuspendedHandler);
this._session.Rendering.OnLostSound += new RenderingSession.LostSoundHandler(this.MyLostSoundHandler);
this._session.OutputDevice.FallbackFontName = this.RootStyleFrame.Constants["FallbackFontFace"];
this._session.Rendering.ImagePartResolver = new ResolveImagePartHandler(this.LoadImage);
this._bgPages = new HybridDictionary();
this.UserConfig.LoadWithUpgrade(typeof(PageBasedUCPUserConfig));
this.UpdateSoundEffectsState();
this.UserConfig.InitOverscanMargins();
ShellData shellData = ShellData.Get(this.Session);
if (shellData != null)
{
shellData.SetBindNavHintsToToolbars(PageBasedUCPService.GlobalConfig.bindNavHintsToToolbars);
}
this._serviceDisplay = new DisplayService(this._session);
this._serviceDisplay.LoadSettings();
this.UpdateAnimationState();
AnimationDescription.CreateRootAnimation("CommonAnimations.xml");
this.UpdateSkin();
this.RefreshHighContrastMode();
this._backPages = new PageBasedUCPService.PageStack(UCPService.s_iStackDepth);
this._backPages.ShouldDisposeHandler = new PageBasedUCPService.PageStack.ShouldDisposeDelegate(this.ShouldDisposePSE);
this.PushTemporaryBackground();
this._notifyWindow = new PageBasedUCPNotifyWindow(this);
bool flag = true;
if (base.GetOption("nochrome"))
{
this._fStandardWindowAdornment = false;
}
this._form = new PageBasedUCPForm(this, this._session, this._fStandardWindowAdornment, flag);
var key = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Media Center\\Settings\\DisplayService");
if (UCPUtility.CheckBoolRegVal(key, "DisableFullscreenOptimizations"))
{
this._form.OptimizeFullscreenMode = false;
}
this._form.PreDestroy += this.OnPreShutdown;
this._form.AppNotifyWindow = this._notifyWindow.Handle;
Win32Api.STARTUPINFO startupinfo = new Win32Api.STARTUPINFO();
Win32Api.GetStartupInfo(startupinfo);
ushort wShowWindow = startupinfo.wShowWindow;
FormWindowState formWindowState;
switch (wShowWindow)
{
case 2:
case 6:
case 7:
break;
case 3:
formWindowState = FormWindowState.Maximized;
goto IL_2FA;
case 4:
case 5:
goto IL_2D1;
default:
if (wShowWindow != 11)
{
goto IL_2D1;
}
break;
}
formWindowState = FormWindowState.Minimized;
goto IL_2FA;
IL_2D1:
if (this.ExtensibilityHostMode)
{
formWindowState = FormWindowState.Normal;
}
else
{
int showCmd = this.UserConfig.showCmd;
if (showCmd == 1 || showCmd == 5)
{
formWindowState = FormWindowState.Normal;
}
else
{
formWindowState = FormWindowState.Maximized;
}
}
IL_2FA:
Point point = new Point(0, 0);
Size size = new Size(0, 0);
this.GetDefaultPositionAndSize(ref point, ref size);
this.ValidateWindowPosition(ref point);
this.ValidateWindowSize(ref size, formWindowState);
if (!Capabilities.Current.IsWindowModeAllowed)
{
formWindowState = FormWindowState.Maximized;
}
this.Form.Position = point;
this.Form.InitialClientSize = size;
this.Form.WindowState = formWindowState;
this.LaunchWindowState = this.Form.WindowState;
this.Form.MinResizeWidth = 230;
this.Form.MaxResizeWidth = -1;
this.UpdateDisplayOverscan(this.UserConfig.OverscanMargins);
if (base.GetOption("alwaysallowscreensaver"))
{
this.Form.PushPreventScreenSaverBlocks();
}
this._rootGadget = new RootGadget(this.RootStyleFrame, UCPService.StringTable, this);
this.UpdateAppBackground();
this._pageHost = new PageHost(this.NotificationPanel, this, this._services);
this._rootGadget.IdealLeft = new SizeSpec(0);
this._rootGadget.IdealTop = new SizeSpec(0);
this._rootGadget.IdealWidth = new SizeSpec(1024);
this._rootGadget.IdealHeight = new SizeSpec(768);
AutoScan.NotifyOnMusicInsertEvent += base.HandleMusicInsertEvent;
AutoScan.NotifyOnAudioCDEvent += this.CdromChangeEvent;
AutoScan.NotifyOnCDEvent += this.NotifyFPDOnCDChangeEvent;
this._loggingTimer = new Microsoft.MediaCenter.UI.Timer();
this._loggingTimer.TimeSpanInterval = TimeSpan.FromMinutes(1.0);
this._loggingTimer.AutoRepeat = true;
this._loggingTimer.Enabled = true;
this.InitTemplates();
PerfInfo.SetMark("ehShell::OnRendererDone");
}
}
}
I added this option as a way to mitigate one of the side effects of running WMC with the fullscreen exclusive mode disabled: the fact the WMC GUI didn't run fullscreen when restarting it. For that, I decided to patch the LocalCapabilities.InitializeDefaultProperties() to force WMC to always run fullscreen when DisableWindowedMode is set to 1:
Code: Select all
using System;
using System.Text;
using Microsoft.MediaCenter.Debug;
using Microsoft.MediaCenter.Interop;
using Microsoft.Win32;
namespace ServiceBus.UIFramework
{
// Token: 0x02000AE9 RID: 2793
internal partial class LocalCapabilities : Capabilities
{
// Token: 0x06006398 RID: 25496 RVA: 0x001973A0 File Offset: 0x001955A0
protected override void InitializeDefaultProperties()
{
StringBuilder stringBuilder = new StringBuilder(260);
int capacity = stringBuilder.Capacity;
try
{
if (UIHelper.EHGetUserName(stringBuilder, ref capacity) == 0)
{
this.m_strClientName = stringBuilder.ToString();
}
}
catch (Exception ex)
{
if (eDebug.FilterException("Unable to get User Name", eDebug.ExceptionType.Ignored, ex))
{
throw;
}
}
this.m_bAreAdvancedPhotoFeaturesAllowed = true;
this.m_bArePopupsAllowed = true;
this.m_bAreVideoZoomModesAllowed = true;
this.m_bIs10FootHelpAllowed = true;
this.m_bIs10FootWebContentAllowed = true;
this.m_bIs2FootHelpAllowed = true;
this.m_bIs2FootWebContentAllowed = true;
this.m_bIsAudioAllowed = true;
this.m_bIsAutoRestartAllowed = true;
this.m_bIsCDBurningAllowed = true;
this.m_bIsCDCopyingAllowed = true;
this.m_bIsCDPlaybackAllowed = true;
this.m_bIsDVDBurningAllowed = true;
this.m_bIsDvdPlaybackAllowed = true;
this.m_bIsFPDAllowed = true;
this.m_bIsHDContentAllowed = true;
this.m_bIsHDContentAllowedByNetwork = true;
this.m_bIsIntensiveAnimationAllowed = true;
this.m_bIs2DAnimationAllowed = false;
this.m_bIsIntensiveHTMLRenderingAllowed = true;
this.m_bIsMyDocumentsPopulated = true;
this.m_bIsOnlineSpotlightAllowed = true;
this.m_bIsSDContentAllowedByNetwork = true;
this.m_bIsToolbarAllowed = true;
this.m_bIsTransferToDeviceAllowed = true;
this.m_bIsTrayAppletAllowed = true;
this.m_bIsUISoundSupported = true;
this.m_bIsVideoAllowed = true;
this.m_bIsVolumeUIAllowed = true;
this.m_bIsMuteUIAllowed = true;
this.m_bIsWin32ContentAllowed = true;
this.m_bIsWmpVisualizationAllowed = true;
this.m_bIsNonLinearZoomSupported = true;
this.m_bIsRawStretchedZoomSupported = false;
this.m_bIsWidescreenSupported = true;
this.m_bIsHighContrastSkinSupported = true;
this.m_strProtocolInfo = "";
this.m_strGetExtenderType = "";
this.m_strGetPakBuildVersion = "";
var key = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Media Center\\Settings\\DisplayService");
if (UCPUtility.CheckBoolRegVal(key, "DisableWindowedMode"))
{
this.m_bIsWindowModeAllowed = false;
}
else
{
this.m_bIsWindowModeAllowed = true;
}
}
}
}
While it's based on the 8.8.5 installer, it should also work with the other branches.
The tool is very easy to use: make sure WIndows Media Center is not running (ehshell + the ehrecvr/ehsched services) and launch Installer.cmd.
Once you've done that, configure the registry keys depending on the options you want to enable. To enable everything, you can create a .reg file with the following content:
Code: Select all
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Media Center\Service\Recording]
"DisableInvalidRecordingsDeletion"=dword:00000001
"WriteAdditionalMetadata"=dword:00000001
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Media Center\Settings\DisplayService]
"DisableVideoOverscan"=dword:00000001
"DisableWindowedMode"=dword:00000001
"DisableFullscreenOptimizations"=dword:00000001
Code: Select all
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Media Center\Service\Recording]
"DisableInvalidRecordingsDeletion"=dword:00000000
"WriteAdditionalMetadata"=dword:00000000
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Media Center\Settings\DisplayService]
"DisableVideoOverscan"=dword:00000000
"DisableWindowedMode"=dword:00000000
"DisableFullscreenOptimizations"=dword:00000000
Of course, I'm open to adding other fixes/workarounds, but please keep in mind they have to stay very focused (e.g changing the EPG to have a completely different look is not a goal of this project)