Jump to content

Help Requested: Plugin dependencies


roaku
Go to solution Solved by roaku,

Recommended Posts

This is mostly just me being ignorant of Visual Studio, C#, and the specifics of Emby's plugin architecture...

But I've got a plugin working in Emby, and it has a runtime dependency on Magick.NET, and I just can't figure out a way to get that dependency to be available to my plugin when it's actually being utilized within Emby.

I always end up with the dependency file not being found, and I've tried copying the dependency to every directory I could think of, and a lot of research gave me a lot of things to try that sounded like they might work, but didn't...

@chef

Log error:

Quote

    *** Error Report ***
    Version: 4.5.3.0
    Command line: [dir]AppData\Roaming\Emby-Server\system\EmbyServer.dll -noautorunwebapp
    Operating system: Microsoft Windows 10.0.18363
    Framework: .NET Core 3.1.8
    OS/Process: x64/x64
    Runtime: [dir]AppData/Roaming/Emby-Server/system/System.Private.CoreLib.dll
    Processor count: 8
    Data path: [dir]AppData\Roaming\Emby-Server\programdata
    Application path: [dir]AppData\Roaming\Emby-Server\system
    System.IO.FileNotFoundException: System.IO.FileNotFoundException: Could not load file or assembly 'Magick.NET-Q8-AnyCPU, Version=7.22.2.0, Culture=neutral, PublicKeyToken=2004825badfa91ec'. The system cannot find the file specified.
...

 

This is and excerpt of what  my .csproj currently looks like. The other two dependencies are available just fine, but I'm guessing that's because they're already in the system.

  <ItemGroup>
    <PackageReference Include="Magick.NET-Q8-AnyCPU" Version="7.22.2.2">
      <IncludeAssets>all</IncludeAssets>		  
    </PackageReference>
    <PackageReference Include="mediabrowser.server.core" Version="4.5.0.28" />
    <PackageReference Include="System.Memory" Version="4.5.4" />
  </ItemGroup>

 

Does anyone mind sharing how, exactly, external dependencies can be packaged with an Emby plugin?

Alternatively, since Emby is clearly using an image manipulation library and most likely backed by ImageMagick...is there a better way for a plugin to tap into image manipulation tools within Emby? I looked through the available classes and interfaces as best I could, but couldn't find a way.

Link to comment
Share on other sites

On 12/12/2020 at 12:35 PM, roaku said:

This is mostly just me being ignorant of Visual Studio, C#, and the specifics of Emby's plugin architecture...

But I've got a plugin working in Emby, and it has a runtime dependency on Magick.NET, and I just can't figure out a way to get that dependency to be available to my plugin when it's actually being utilized within Emby.

I always end up with the dependency file not being found, and I've tried copying the dependency to every directory I could think of, and a lot of research gave me a lot of things to try that sounded like they might work, but didn't...

@chef

Log error:

 

This is and excerpt of what  my .csproj currently looks like. The other two dependencies are available just fine, but I'm guessing that's because they're already in the system.


  <ItemGroup>
    <PackageReference Include="Magick.NET-Q8-AnyCPU" Version="7.22.2.2">
      <IncludeAssets>all</IncludeAssets>		  
    </PackageReference>
    <PackageReference Include="mediabrowser.server.core" Version="4.5.0.28" />
    <PackageReference Include="System.Memory" Version="4.5.4" />
  </ItemGroup>

 

Does anyone mind sharing how, exactly, external dependencies can be packaged with an Emby plugin?

Alternatively, since Emby is clearly using an image manipulation library and most likely backed by ImageMagick...is there a better way for a plugin to tap into image manipulation tools within Emby? I looked through the available classes and interfaces as best I could, but couldn't find a way.

Dependencies are tricky, because anything the plugin depends on will have to be put in the Emby-Server folder.

The problem is, that anything you put in there will disappear when emby updates.

There are two ways around this.

1. Find the project (hopefully open source) and copy then entire class system into your plugin. Now your plugin doesn't need to be dependent on anything but itself.

 

2. Copy the binary dll for Magik into your plugin as an embedded resource, and use the IServerEntry point 'Run()' method to look in embys root and see if it exists on load.

If not, use Reflection to copy the embedded resource into the appropriate folder location, and you are going to have to restart the server again to load the dependency.

 

I vote for number 1. It is probably the best. You'll have to restart the server twice after update, the second way.

 

Let me know if you have any other questions.

  • Thanks 1
Link to comment
Share on other sites

It's been a while since I fiddled with it but I'm pretty sure  you can load the dependency directly from your resource (without the need for the file copy or double restart).

However, minimizing the dependencies is still the best option.

  • Thanks 1
Link to comment
Share on other sites

Thanks for looking at this @chef @ebr

So theoretically, if I'm doing everything right in my project definition...and the dll I'm depending on is present in '\EmbyServer\system'...it should be available to my plugin at runtime? Or should it be in another directory?

I've got that file not found error, but I can't figure out what directories that class loader is scanning when it fails to find my dependency file.
 

I'm not disregarding the advice to include the dependent source code directly, but that quickly escalated into what felt like a bigger problem to figure out in its own right, and I'm just trying to work out a proof of concept at this point.

Edited by roaku
  • Like 1
Link to comment
Share on other sites

3 hours ago, roaku said:

Thanks for looking at this @chef @ebr

So theoretically, if I'm doing everything right in my project definition...and the dll I'm depending on is present in '\EmbyServer\system'...it should be available to my plugin at runtime? Or should it be in another directory?

I've got that file not found error, but I can't figure out what directories that class loader is scanning when it fails to find my dependency file.
 

I'm not disregarding the advice to include the dependent source code directly, but that quickly escalated into what felt like a bigger problem to figure out in its own right, and I'm just trying to work out a proof of concept at this point.

yes "Emby-Server/system" is the correct folder.

The server needs to load it when it starts, so make sure it's there when you start the server.

But, quickly what ebr mentioned is a third way of doing this. It's a bit more tricky, but if you Google: "How to load dependency dll from embedded resource", something might come up.

In fact, it's such an interesting idea, that if you figure that out, I'd love to know.

  • Thanks 1
Link to comment
Share on other sites

  • Solution

Well, I've done it. I've managed to absolutely mangle Russell Crowe with Magick.NET by way of an Emby plugin implementing MediaBrowser.Controller.Providers.IImageEnhancer. :)

Primary.png.3014351f2c9057ecb372107d94c0a740.png

This isn't what I actually want to do with the plugin, but this was a major hurdle between me and getting there.

 

Here's what worked:

I found https://github.com/dotnet/ILMerge and built that exe locally.

Then, I was able to configure it in my csproj and tell it to add all the *managed* dlls to one combined dll:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>netstandard2.0;</TargetFrameworks>
    <AssemblyVersion>1.0.0.0</AssemblyVersion>
    <FileVersion>1.0.0.0</FileVersion>
    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
    <OutputType>Library</OutputType>
	  <ILMergeConsolePath>[pathto]\ILMerge.exe</ILMergeConsolePath>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="ILMerge" Version="3.0.41" />
    <PackageReference Include="Magick.NET-Q8-AnyCPU" Version="7.22.2.2" />
    <PackageReference Include="mediabrowser.server.core" Version="4.5.0.28" />
	<PackageReference Include="System.Memory" Version="4.5.4" />
  </ItemGroup>

  <ItemGroup>
    <None Remove="Configuration\ImageAudioEnhancerConfiguration.html" />
    <None Remove="thumb.jpg" />
  </ItemGroup>

  <ItemGroup>
    <EmbeddedResource Include="Configuration\ImageAudioEnhancerConfiguration.html" />
    <EmbeddedResource Include="thumb.jpg" />
  </ItemGroup>

    <ItemGroup>
	    <PackageReference Include="ILMerge" Version="3.0.41" />
    </ItemGroup>
	
	

	<Target Name="ILMerge">
		<Exec Command="$(ILMergeConsolePath) /out:Merged.dll [pathtodll1]\ImageAudioEnhancer.dll [pathtodll2]\Magick.NET.Core.dll [pathtodll3]\Magick.NET-Q8-AnyCPU.dll" />
	</Target>
</Project>

 

After that, I could run 'msbuild /t:ILMerge' from the Visual Studio command line, which generated the merged dll (I'm pretty sure this can be automated, but I haven't tried that yet).

Copying that merged dll over to the Emby plugins directory...still didn't work. Emby still blew up on image processing, but now it was because an entirely different dependency was missing: 'Magick.Native-Q8-x64.dll'. The Magick.NET dependency was finally calling code!

This is where I learned the difference between *managed* assemblies and *native* assemblies.

Turns out, ILMerge can only combine the managed ones.

But, copying 'Magick.Native-Q8-x64.dll' over to Emby-Server\system worked *this* time. Emby was able to pick it up and use it when the Image processors ran.

Charcoal Crowe!

 

My understanding now is that the Magick.NET dependencies I was copying over to system earlier were incompatible because they were the wrong type of dll for that approach. Those needed to be integrated with my plugin at build time.

Embedding the native Magick assembly and using reflection to drop it in if it's missing will probably work for that case, but I'm not worried about that right now. I'll also have to make sure to grab the correct build of it since I hope to get this on my Synology eventually.

 

Thanks for your help on this @chef.and @ebr  I'm used to Maven in Java-land and Composer with PHP and just not having to think too hard about dependent libraries being available when you need them. :)

 

Primary.png

Edited by roaku
  • Like 1
Link to comment
Share on other sites

That's cool!

Did you know that the coverart plugin uses imageMagikabd that there might even be a Interface somewhere in emby which handles image processing?

 

 

Link to comment
Share on other sites

37 minutes ago, chef said:

That's cool!

Did you know that the coverart plugin uses imageMagikabd that there might even be a Interface somewhere in emby which handles image processing?

 

 

I mean, this is from my first post in the thread...so I'm all ears if there's a more efficient approach. :)

On 12/12/2020 at 11:35 AM, roaku said:

Alternatively, since Emby is clearly using an image manipulation library and most likely backed by ImageMagick...is there a better way for a plugin to tap into image manipulation tools within Emby? I looked through the available classes and interfaces as best I could, but couldn't find a way.

My plugin *is* implementing MediaBrowser.Controller.Providers.IImageEnhancer, so it is firing off per image request to the API, so there's that at least.

 

I've actually requested the feature I'm working on be added to the CoverArt plugin. If that was open source, I'd be working toward a PR, not a parallel implementation that I hope can eventually get ported over to CoverArt by its maintainers. 🙈

 

 

Edited by roaku
  • Like 1
Link to comment
Share on other sites

Make sure that  you implement the cache key for your image processor so that it doesn't have to run on every image request.

  • Like 1
Link to comment
Share on other sites

59 minutes ago, ebr said:

Make sure that  you implement the cache key for your image processor so that it doesn't have to run on every image request.

Thanks.

I had implemented that method, having it just return '$"{item.Id}_{imageType.GetType().Name}_enhanced"'.

I wasn't sure what it was actually wanting beyond a string that somewhat uniquely identifies the image.

Checking up on it now, I see a copy of my enhanced image in the cache:

\Emby-Server\programdata\cache\images\enhanced-images\0\03db6ceeb515f7f8f6d28454f1b5a0d4.png

And the debug log shows:

2020-12-15 15:47:20.799 Debug Skia: Skia returning original image C:\Users\jason\AppData\Roaming\Emby-Server\programdata\cache\images\enhanced-images\0\03db6ceeb515f7f8f6d28454f1b5a0d4.png

 

So, I think caching is good...

Let me know if there's something better to be returning for the cache key string.

Link to comment
Share on other sites

That looks like any individual image will only ever get processed once.  If the media changes, then your plug-in will never re-process it.  Your cache key needs to be based on whether or not the processor should be run.

So, for instance, if you are putting an overlay for audio type on the image, then the cache key should incorporate the specific audio type being displayed.  That way, if it ever changes, your processor will automatically re-render the overlay. 

  • Thanks 1
Link to comment
Share on other sites

Well, this is pretty cool.

We're successfully checking the item for commentaries and scores based on the title tag of the Audio type MediaStreams and overlaying icons based on what we find.

Still have a lot of work to do but we've reached the 'technically working' stage!

technically-working.thumb.png.71a9e2f4f074b8e2366897474416a24b.png

Link to comment
Share on other sites

  • 2 months later...

Just wanted to add to this thread in case anyone else finds this while searching since I had the same issue but the solution above didn't work for me.

I wanted to use Entity Framework Core in my plugin which unfortunately comes with a lot of dependencies.

At first I tried copying the dlls. to the system folders but same as for roaku above that didn't work probably since they were managed assemblies.

 

The solution above didn't work for me since the large number of dlls caused issues with ILMerge and I just got a bunch of different error messages.

Instead I found Costura which also merges dependencies but seems to work a lot better with the large number of dependencies than ILMerge.

So if anyone else finds this thread with the same issue that might help.

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...